HandleBase 详解 - 控制点基类

这是形状系统中控制点的抽象基类,为所有具体的控制点(角点、边中点、旋转点等)提供了通用的位置管理、绘制和命中检测功能。


📄 文件头部和引用

// 版权信息(同前)
// Copyright (c) HeBianGu Authors. All Rights Reserved. 
// ...(省略)

using System.Windows.Input;
// 导入光标相关类型(Cursor)

namespace H.LabelImg.ShapeBox.Shapes.Handles;
// 命名空间:存放形状的控制点相关类

🏗️ HandleBase 抽象类

// 抽象基类:为所有控制点提供通用实现
// 实现 IHandle 接口
public abstract class HandleBase : IHandle
{
    // 构造函数:创建控制点时调用
    // 参数:
    //   postion - 控制点的位置(坐标)
    //   shape - 控制点所属的形状
    public HandleBase(Point postion, IShape shape)
    {
        this.Postion = postion;  // 设置位置
        this.Owner = shape;       // 设置所属形状
    }
    
    // 所属形状(只读,只能在构造函数中设置)
    public IShape Owner { get; }
    
    // 控制点位置(可读写)
    // 注意:原代码拼写为 Postion(应该是 Position)
    public Point Postion { get; set; }
    
    // 控制点的大小(边长/直径,默认6像素)
    public double Length { get; set; } = 6.0;
    
    // 鼠标悬停时的光标样式(可被子类重写)
    public virtual Cursor Cursor { get; set; }
    
    // 绘制控制点(虚方法,子类可重写)
    public virtual void Draw(IView view, DrawingContext drawingContext, 
                              Pen pen, Brush fill = null)
    {
        // 调用扩展方法绘制控制点
        // DrawHandle 会绘制一个小矩形(默认白色填充,边框使用传入的画笔)
        drawingContext.DrawHandle(
            this.Postion,           // 控制点位置
            pen.Brush,              // 边框颜色
            pen.Thickness,          // 边框粗细
            this.Length / view.Scale  // 控制点大小(考虑缩放)
        );
    }
    
    // 命中检测:判断点是否点击了控制点
    public virtual bool HitTestPoint(IView view, Point position)
    {
        // 计算控制点的半长(考虑缩放)
        // view.Scale 是视图缩放比例
        // 缩放越大,控制点的命中区域越小(保持视觉一致性)
        var d = 0.5 * this.Length / view.Scale;
        
        // 计算控制点位置与点击位置的偏移
        var v = this.Postion - position;
        
        // 检查点击位置是否在控制点的矩形范围内
        // 条件:X偏移和Y偏移的绝对值都小于半长
        return Math.Abs(v.X) < d && Math.Abs(v.Y) < d;
    }
}

🎯 核心功能详解

1. 构造函数 - 建立控制点与形状的关联

public HandleBase(Point postion, IShape shape)
{
    this.Postion = postion;
    this.Owner = shape;
}

作用:每个控制点都必须知道:

  • 它在哪里(位置)
  • 它属于谁(所属形状)
形状(矩形)                     控制点(角点)
┌─────────────────┐             
│                 │             Owner ──→ 矩形
│                 │             Position ──→ (100, 100)
│                 │             
└─────────────────┘             
● ← 控制点位置 (100, 100)

2. Length 属性 - 控制点大小

public double Length { get; set; } = 6.0;

视觉效果

Length = 4:        Length = 6:        Length = 8:
  ┌──┐               ┌────┐              ┌──────┐
  │  │               │    │              │      │
  └──┘               └────┘              └──────┘
  (小)               (中)                 (大)

3. Draw 方法 - 绘制控制点

public virtual void Draw(IView view, DrawingContext drawingContext, 
                          Pen pen, Brush fill = null)
{
    drawingContext.DrawHandle(
        this.Postion, 
        pen.Brush, 
        pen.Thickness, 
        this.Length / view.Scale  // ← 关键:考虑缩放
    );
}

缩放处理的重要性

视图缩放 = 1.0(原始大小)    视图缩放 = 2.0(放大2倍)
Length = 6                   Length / Scale = 6 / 2 = 3

显示大小:6像素              显示大小:3像素
┌────┐                      ┌──┐
│    │                      │  │
└────┘                      └──┘

视觉效果:大小适中           视觉效果:大小适中
(不缩放会显示为12像素,太大!)

4. HitTestPoint 方法 - 命中检测

public virtual bool HitTestPoint(IView view, Point position)
{
    var d = 0.5 * this.Length / view.Scale;
    var v = this.Postion - position;
    return Math.Abs(v.X) < d && Math.Abs(v.Y) < d;
}

几何原理

控制点区域(正方形):
        
        d = 半长
        ←───→
    ┌─────────┐
    │    ●    │ ← 控制点中心 (Postion)
    │         │
    └─────────┘
    
命中条件:
|Postion.X - position.X| < d  AND
|Postion.Y - position.Y| < d

即:点击位置在控制点正方形内

示例计算

// 假设缩放为 1.0,Length = 6
d = 0.5 * 6 / 1.0 = 3

控制点中心 (100, 100)
命中区域:X 在 97~103,Y 在 97~103

点击 (100, 100) → 命中 ✅
点击 (95, 100)  → X差=5,大于3 → 未命中 ❌
点击 (100, 95)  → Y差=5,大于3 → 未命中 ❌

📝 具体控制点实现示例

示例1:角点控制点(继承 HandleBase)

// 矩形角点控制点
public class CornerHandle : HandleBase
{
    private RectangleShape _rectangle;
    private CornerType _cornerType;
    
    public CornerHandle(RectangleShape rectangle, CornerType cornerType, Point position) 
        : base(position, rectangle)
    {
        _rectangle = rectangle;
        _cornerType = cornerType;
        
        // 设置光标样式
        switch (cornerType)
        {
            case CornerType.TopLeft:
            case CornerType.BottomRight:
                this.Cursor = Cursors.SizeNWSE;  // ↖↘
                break;
            case CornerType.TopRight:
            case CornerType.BottomLeft:
                this.Cursor = Cursors.SizeNESW;  // ↗↙
                break;
        }
    }
    
    // 更新形状的方法(在拖拽时调用)
    public void UpdateShape(Vector delta)
    {
        var bounds = _rectangle.Bounds;
        
        switch (_cornerType)
        {
            case CornerType.TopLeft:
                bounds = new Rect(
                    bounds.Left + delta.X, 
                    bounds.Top + delta.Y,
                    bounds.Width - delta.X, 
                    bounds.Height - delta.Y
                );
                break;
            case CornerType.TopRight:
                bounds = new Rect(
                    bounds.Left, 
                    bounds.Top + delta.Y,
                    bounds.Width + delta.X, 
                    bounds.Height - delta.Y
                );
                break;
            // ... 其他角点类似
        }
        
        if (bounds.Width > 0 && bounds.Height > 0)
        {
            _rectangle.Bounds = bounds;
            // 更新控制点位置
            this.Postion = GetPosition();
        }
    }
    
    private Point GetPosition()
    {
        var bounds = _rectangle.Bounds;
        switch (_cornerType)
        {
            case CornerType.TopLeft: return new Point(bounds.Left, bounds.Top);
            case CornerType.TopRight: return new Point(bounds.Right, bounds.Top);
            case CornerType.BottomLeft: return new Point(bounds.Left, bounds.Bottom);
            case CornerType.BottomRight: return new Point(bounds.Right, bounds.Bottom);
            default: return new Point(0, 0);
        }
    }
}

示例2:边中点控制点

public class EdgeHandle : HandleBase
{
    private RectangleShape _rectangle;
    private EdgeType _edgeType;
    
    public EdgeHandle(RectangleShape rectangle, EdgeType edgeType, Point position) 
        : base(position, rectangle)
    {
        _rectangle = rectangle;
        _edgeType = edgeType;
        
        // 设置光标样式
        switch (edgeType)
        {
            case EdgeType.Top:
            case EdgeType.Bottom:
                this.Cursor = Cursors.SizeNS;  // ↕ 垂直
                break;
            case EdgeType.Left:
            case EdgeType.Right:
                this.Cursor = Cursors.SizeWE;  // ↔ 水平
                break;
        }
    }
    
    public void UpdateShape(Vector delta)
    {
        var bounds = _rectangle.Bounds;
        
        switch (_edgeType)
        {
            case EdgeType.Top:
                bounds = new Rect(
                    bounds.Left, 
                    bounds.Top + delta.Y,
                    bounds.Width, 
                    bounds.Height - delta.Y
                );
                break;
            case EdgeType.Bottom:
                bounds = new Rect(
                    bounds.Left, 
                    bounds.Top,
                    bounds.Width, 
                    bounds.Height + delta.Y
                );
                break;
            case EdgeType.Left:
                bounds = new Rect(
                    bounds.Left + delta.X, 
                    bounds.Top,
                    bounds.Width - delta.X, 
                    bounds.Height
                );
                break;
            case EdgeType.Right:
                bounds = new Rect(
                    bounds.Left, 
                    bounds.Top,
                    bounds.Width + delta.X, 
                    bounds.Height
                );
                break;
        }
        
        if (bounds.Width > 0 && bounds.Height > 0)
        {
            _rectangle.Bounds = bounds;
            this.Postion = GetPosition();
        }
    }
    
    private Point GetPosition()
    {
        var bounds = _rectangle.Bounds;
        switch (_edgeType)
        {
            case EdgeType.Top:
                return new Point(bounds.Left + bounds.Width / 2, bounds.Top);
            case EdgeType.Bottom:
                return new Point(bounds.Left + bounds.Width / 2, bounds.Bottom);
            case EdgeType.Left:
                return new Point(bounds.Left, bounds.Top + bounds.Height / 2);
            case EdgeType.Right:
                return new Point(bounds.Right, bounds.Top + bounds.Height / 2);
            default:
                return new Point(0, 0);
        }
    }
}

示例3:圆形控制点

public class CircleHandle : HandleBase
{
    private CircleShape _circle;
    
    public CircleHandle(CircleShape circle, Point position) 
        : base(position, circle)
    {
        _circle = circle;
        this.Cursor = Cursors.SizeNWSE;
    }
    
    public void UpdateShape(Vector delta)
    {
        // 根据拖拽改变半径
        double newRadius = _circle.Radius + delta.X;
        if (newRadius > 5)
        {
            _circle.Radius = newRadius;
            // 更新控制点位置(保持在圆的最右边)
            this.Postion = new Point(_circle.Center.X + _circle.Radius, _circle.Center.Y);
        }
    }
}

🎨 自定义控制点示例

带特殊绘制的控制点

public class SpecialHandle : HandleBase
{
    public SpecialHandle(Point position, IShape shape) : base(position, shape)
    {
        this.Length = 10;  // 更大一点
        this.Cursor = Cursors.Hand;
    }
    
    // 重写绘制方法
    public override void Draw(IView view, DrawingContext dc, Pen pen, Brush fill = null)
    {
        // 绘制一个圆形而不是矩形
        double radius = this.Length / view.Scale / 2;
        dc.DrawEllipse(fill ?? Brushes.Yellow, pen, this.Postion, radius, radius);
    }
    
    // 重写命中检测(圆形区域)
    public override bool HitTestPoint(IView view, Point position)
    {
        double radius = this.Length / view.Scale / 2;
        double dx = this.Postion.X - position.X;
        double dy = this.Postion.Y - position.Y;
        double distance = Math.Sqrt(dx * dx + dy * dy);
        return distance <= radius;
    }
}

🔄 在形状中的使用

public class RectangleShape : HandleShapeBase
{
    public Rect Bounds { get; set; }
    
    protected override IEnumerable<IHandle> CreateHandles()
    {
        // 创建8个控制点
        yield return new CornerHandle(this, CornerType.TopLeft, 
            new Point(Bounds.Left, Bounds.Top));
        yield return new EdgeHandle(this, EdgeType.Top,
            new Point(Bounds.Left + Bounds.Width / 2, Bounds.Top));
        yield return new CornerHandle(this, CornerType.TopRight,
            new Point(Bounds.Right, Bounds.Top));
        // ... 其他控制点
    }
    
    // 当形状改变时更新控制点位置
    public void UpdateHandles()
    {
        foreach (var handle in CreateHandles())
        {
            if (handle is CornerHandle corner)
                corner.Postion = GetCornerPosition(corner.CornerType);
            // ... 更新其他控制点
        }
    }
}

设计模式分析

1. 模板方法模式

基类定义了控制点的通用行为(绘制、命中检测),子类可以重写特定行为。

2. 依赖注入

通过构造函数注入所属形状,实现控制点与形状的关联。

3. 策略模式

不同的控制点实现不同的拖拽策略(角点改变大小、边中点单向拉伸等)。


总结

成员 类型 用途
Owner 属性 控制点所属的形状
Postion 属性 控制点位置(支持动态更新)
Length 属性 控制点大小(默认6像素)
Cursor 属性 鼠标光标样式
Draw() 方法 绘制控制点(考虑缩放)
HitTestPoint() 方法 检测点击(考虑缩放)

核心设计亮点

  1. 缩放自适应:控制点大小和命中区域随视图缩放自动调整
  2. 位置可更新:Postion 属性可读写,支持形状变化后更新
  3. 可扩展性:虚方法允许子类自定义绘制和命中检测
  4. 代码复用:基类提供了80%的通用功能

这个基类大大简化了控制点的实现,添加新类型控制点只需继承并重写少量方法!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐