C# WinForm中实现PictureBox内图形绘制、拖拽调整大小及移动的方法
实现思路
- 先定义图形实体类:存储矩形的位置、大小、选中状态,封装控制点计算、命中测试逻辑
- 统一绘图逻辑:所有绘制操作放在PictureBox的
Paint事件中执行,避免重绘后内容丢失 - 绑定三个核心鼠标事件:
MouseDown(交互开始)、MouseMove(交互过程)、MouseUp(交互结束),分别实现选中、拖拽移动、尺寸调整逻辑
核心实现代码
1. 矩形图形类定义
public class RectShape { public Rectangle Bounds { get; set; } public bool IsSelected { get; set; } // 控制点固定大小 private const int HandleSize = 8; // 获取5个控制点:左上、右上、左下、右下、中上 public List<Rectangle> GetHandles() { var handles = new List<Rectangle> { // 左上 new Rectangle(Bounds.Left - HandleSize/2, Bounds.Top - HandleSize/2, HandleSize, HandleSize), // 右上 new Rectangle(Bounds.Right - HandleSize/2, Bounds.Top - HandleSize/2, HandleSize, HandleSize), // 左下 new Rectangle(Bounds.Left - HandleSize/2, Bounds.Bottom - HandleSize/2, HandleSize, HandleSize), // 右下 new Rectangle(Bounds.Right - HandleSize/2, Bounds.Bottom - HandleSize/2, HandleSize, HandleSize), // 中上 new Rectangle(Bounds.Left + Bounds.Width/2 - HandleSize/2, Bounds.Top - HandleSize/2, HandleSize, HandleSize) }; return handles; } // 判断点是否在矩形内部 public bool Contains(Point p) => Bounds.Contains(p); // 命中测试控制点,返回索引,-1表示未命中 public int HitTestHandle(Point p) { var handles = GetHandles(); for (int i = 0; i < handles.Count; i++) { if (handles[i].Contains(p)) return i; } return -1; } }
2. 窗体全局变量定义
// 存储所有绘制的矩形 private List<RectShape> _shapes = new List<RectShape>(); // 当前选中的矩形 private RectShape _selectedShape; // 当前拖拽的控制点索引,-1为未拖拽 private int _dragHandleIndex = -1; // 拖拽移动时的鼠标偏移量 private Point _dragOffset; // 是否正在拖拽移动图形 private bool _isDragging = false;
3. Paint事件绘制逻辑
private void pictureBox1_Paint(object sender, PaintEventArgs e) { e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // 绘制所有矩形 foreach (var shape in _shapes) { e.Graphics.DrawRectangle(Pens.Black, shape.Bounds); // 绘制选中状态的控制点 if (shape.IsSelected) { foreach (var handle in shape.GetHandles()) { e.Graphics.FillRectangle(Brushes.White, handle); e.Graphics.DrawRectangle(Pens.Black, handle); } } } }
4. 鼠标事件处理
MouseDown事件
private void pictureBox1_MouseDown(object sender, MouseEventArgs e) { if (e.Button != MouseButtons.Left) return; // 优先判断是否命中控制点 if (_selectedShape != null) { _dragHandleIndex = _selectedShape.HitTestHandle(e.Location); if (_dragHandleIndex != -1) return; } // 未命中控制点,判断是否命中图形 _selectedShape = null; // 倒序遍历,优先选中最上层图形 for (int i = _shapes.Count - 1; i >= 0; i--) { if (_shapes[i].Contains(e.Location)) { _selectedShape = _shapes[i]; _dragOffset = new Point(e.X - _selectedShape.Bounds.X, e.Y - _selectedShape.Bounds.Y); _isDragging = true; break; } } // 更新选中状态 foreach (var shape in _shapes) shape.IsSelected = shape == _selectedShape; pictureBox1.Invalidate(); }
MouseMove事件
private void pictureBox1_MouseMove(object sender, MouseEventArgs e) { if (_selectedShape == null) return; // 控制点拖拽调整尺寸逻辑 if (_dragHandleIndex != -1) { var bounds = _selectedShape.Bounds; switch (_dragHandleIndex) { case 0: // 左上 bounds.X = e.X; bounds.Y = e.Y; bounds.Width = _selectedShape.Bounds.Right - e.X; bounds.Height = _selectedShape.Bounds.Bottom - e.Y; break; case 1: // 右上 bounds.Y = e.Y; bounds.Width = e.X - _selectedShape.Bounds.Left; bounds.Height = _selectedShape.Bounds.Bottom - e.Y; break; case 2: // 左下 bounds.X = e.X; bounds.Width = _selectedShape.Bounds.Right - e.X; bounds.Height = e.Y - _selectedShape.Bounds.Top; break; case 3: // 右下 bounds.Width = e.X - _selectedShape.Bounds.Left; bounds.Height = e.Y - _selectedShape.Bounds.Top; break; case 4: // 中上 bounds.Y = e.Y; bounds.Height = _selectedShape.Bounds.Bottom - e.Y; break; } // 最小尺寸限制,避免拉成负面积 bounds.Width = Math.Max(5, bounds.Width); bounds.Height = Math.Max(5, bounds.Height); _selectedShape.Bounds = bounds; pictureBox1.Invalidate(); return; } // 图形拖拽移动逻辑 if (_isDragging) { _selectedShape.Bounds = new Rectangle( e.X - _dragOffset.X, e.Y - _dragOffset.Y, _selectedShape.Bounds.Width, _selectedShape.Bounds.Height); pictureBox1.Invalidate(); } // 切换鼠标样式 var handleIndex = _selectedShape.HitTestHandle(e.Location); pictureBox1.Cursor = handleIndex switch { 0 => Cursors.SizeNWSE, 1 => Cursors.SizeNESW, 2 => Cursors.SizeNESW, 3 => Cursors.SizeNWSE, 4 => Cursors.SizeNS, _ => _selectedShape.Contains(e.Location) ? Cursors.SizeAll : Cursors.Default }; }
MouseUp事件
private void pictureBox1_MouseUp(object sender, MouseEventArgs e) { _dragHandleIndex = -1; _isDragging = false; }
补充说明
- 如果需要新增矩形绘制功能,只需要在
MouseDown未命中现有图形时记录起点,MouseUp时计算终点,往_shapes集合添加新的RectShape实例即可 - 如果需要支持其他图形(椭圆、多边形等),可以将
RectShape改为抽象基类,不同图形继承实现各自的绘制、命中测试逻辑 - 可根据需求调整控制点的数量、位置、样式,以及对应的尺寸调整逻辑
内容的提问来源于stack exchange,提问作者Kelvin Teoh
相关产品推荐
相关产品推荐

