You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.29 12:36:06