前言
- 本文将介绍如何使用C#实现自定义图像窗体控件,并在图像窗体控件中绘制和管理不同形状图形的功能,后面都将形状图形描述为ROI。
- 自定义图像窗体控件UImageWindow,继承UImage(在我之前发表的文章中有介绍)。
- 主要介绍的是如何实现在图像控件上绘制及管理ROI。代码中使用的是List集合存储ROI。
- 通过创建ROI基类,并使用List集合存储管理,在创建不同的ROI对象后将其添加到集合即可。如果需要实现创建新的ROI形状类型,可以通过继承ROI基类实现,达到统一管理的效果。
- 目前实现矩形、方向矩形、圆形等形状的ROI绘制(后续更新)。
绘制及管理过程:
- 1、创建抽象基类:
public abstract class ShapeBase;
- 2、创建类继承基类:
public class URectangle:ShapeBase;
- 3、创建ROI集合:
List<ShapeBase> ShapeList;
- 4、添加ROI到集合:
ShapeList.Add(new URectangle());
运行环境
- 操作系统: Windows 11
- 编程软件: Visual Studio 2022
- .Net版本: .Net Framework 4.8.0
项目结构
- CSharp 学习之 ROI 绘制功能实现
├── RoiControllers
│ ├── ShapeBase.cs
│ ├── ShapeUpdateEvents.cs
│ ├── URCircle.cs
│ ├── URectangle.cs
│ └── URectangle2.cs
├── UserControls
│ ├── UImage.cs
│ └── UImageWindow.cs
├── App.config
├── MainForm.cs
└── Program.cs
运行效果
使用方法
- 在ROI基类ShapeBase中创建常用的方法(抽象方法),在子类中重写具体实现逻辑,即可实现在图像窗体控件中使用List集合统一管理不同形状的ROI。
- 创建ROI时,绑定方法到对应的委托事件中,实现操作ROI时获取ROI的数据。
- 下面使用方法,通过绑定ROI变更委托示例,分别注册了附加、选中、移动、调整大小、旋转等操作变更方法,并将创建的ROI对象附加到窗体上,即可实现ROI的添加,同时在变更时实时反馈数据,代码如下:
private void AddShape(ShapeBase shape)
{shape.OnAttach(OnAttchROI); shape.OnSelected(OnSelectedROI);shape.OnMove(OnMoveROI);shape.OnResize(OnResizeROI);shape.OnRotate(OnROtateROI);uImageWindow.AttachShapeToWindow(shape);uImageWindow.AddShape(shape);
}private void OnAttchROI(ShapeBase shape, PointF f)
{UpdateMessage($"添加形状,{shape.Description}=>{shape.Type}");PrintShapeData(shape);
}
private void OnMoveROI(ShapeBase shape, PointF f)
{UpdateMessage($"移动形状,{shape.Description}=>{shape.Type}");PrintShapeData(shape);
}
private void OnResizeROI(ShapeBase shape, PointF f)
{UpdateMessage($"调整形状大小,{shape.Description}=>{shape.Type}");PrintShapeData(shape);
}
private void OnROtateROI(ShapeBase shape, PointF f)
{UpdateMessage($"旋转形状,{shape.Description}=>{shape.Type}");PrintShapeData(shape);
}
private void OnSelectedROI(ShapeBase shape, PointF f)
{UpdateMessage($"选中形状,{shape.Description}=>{shape.Type}");PrintShapeData(shape);
}
统一管理ROI
- 根据鼠标按下、鼠标移动、鼠标抬起等事件操作,判断执行不同的操作。实现检测是否在ROI范围内、就可以通过鼠标点所在的位置判断,以此进行ROI的拖动、调整大小、旋转、选中(选中逻辑在图像窗体中处理)。
- 根据鼠标所在位置,也可以做更多的细节处理,比如显示不同的鼠标光标、可以提供更好的视觉交互的效果(上下左右箭头、手柄等等)。
- 至于具体的方法逻辑、在对应的形状类中实现,这就是使用抽象方法的好处,在子类中重写,在图像窗体中只需要使用基类实例调用方法执行。
#region 抽象方法:绘制形状相关方法。
/// <summary> 调整形状角度 </summary>
public abstract void Rotate(PointF point);
/// <summary> 调整形状位置 </summary>
public abstract void Move(PointF point);
/// <summary> 调整形状大小 </summary>
public abstract void Resize(PointF point);
/// <summary> 绘制形状带手柄:选中时绘制手柄,未选中时绘制轮廓 </summary>
public abstract void DrawShapeWithHandle(Graphics graphics);
/// <summary>绘制形状手柄 </summary>
public abstract void DrawHandle(Graphics graphics, int x, int y);
/// <summary>绘制形状手柄 </summary>
public abstract void DrawHandle(Graphics graphics, float x, float y);
/// <summary>绘制形状手柄 </summary>
public abstract void DrawHandle(Graphics graphics, PointF point);
/// <summary> 获取要显示的自定义手柄类型:根据输入点</summary>
public abstract ShapeHandleType GetHandleType(PointF point);
/// <summary> 获取系统自带的手柄类型:根据输入点 </summary>
public abstract Cursor GetSystemHandleType(PointF point);
/// <summary> 输入点是否在矩形区域内 </summary>
public abstract bool IsInRegion(PointF point);
/// <summary> 输入点是否在矩形背景区域内(仅背景,不包括手柄交集) </summary>
public abstract bool IsInBackground(PointF point);
/// <summary> 输入点是否在手柄范围内(输入点,所在点(中心)) </summary>
public abstract bool IsInHandle(PointF inPoint, PointF point);
/// <summary> 输入点是否在旋转手柄范围内 </summary>
public abstract bool IsInRotateHandle(PointF point);
#endregion
交互逻辑
- 交互逻辑的实现是在图像窗体控件中实现OnMouseDown、OnMouseUp、OnMouseMove、方法进行。
- 在OnMouseDown中判断ROI设置操作。
- 在OnMouseUp中取消ROI操作设置。
- 在OnMouseMove中根据鼠标按下时的操作进行ROI操作。
- 在OnPaint方法中重新绘制ROI。
#region 事件方法重写
protected override void OnPaint(PaintEventArgs e)
{base.OnPaint(e);foreach (ShapeBase shape in ShapeFigureList){shape?.DrawShapeWithHandle(e.Graphics);}
}
protected override void OnMouseDown(MouseEventArgs e)
{base.OnMouseDown(e);if (e.Button == MouseButtons.Left){ShapeFigureList.FindAll(obj => obj.IsSelected = false);foreach (ShapeBase shape in ShapeFigureList){//是否在矩形区域内if (shape.IsInRegion(e.Location)){IsOperateImage = false;_currentShape = shape;_currentShape.IsSelected = true;shape.ExceteSelectedCallback(shape);index = 0;shape.LastLocation = e.Location;//在背景if (shape.IsInBackground(e.Location))DrawModel = ShapeOperateMode.Move;//在旋转手柄上else if (shape.IsInRotateHandle(e.Location)){shape.IsRotating = true;shape.LastLocation = e.Location;DrawModel = ShapeOperateMode.Rotote;}//在手柄elseDrawModel = ShapeOperateMode.Resize;_isDraging = true;shape.HandleType = shape.GetHandleType(e.Location);break;}}}else if (e.Button == MouseButtons.Right){DrawModel = ShapeOperateMode.None;}this.Invalidate();
}
protected override void OnMouseUp(MouseEventArgs e)
{base.OnMouseUp(e);DrawModel = ShapeOperateMode.None;IsOperateImage = true;this.Cursor = Cursors.Default;if (_currentShape!=null) _currentShape.IsRotating= false;shapeFigureList.ForEach(shape => shape.HandleType = ShapeHandleType.None);_isDraging = false;
}
protected override void OnMouseMove(MouseEventArgs e)
{base.OnMouseMove(e);bool isInRegion = false;//是否在矩形区域内foreach (ShapeBase shape in ShapeFigureList){if (shape.IsInRegion(e.Location)){this.Cursor = shape.GetSystemHandleType(e.Location);isInRegion = true;break;}}if (!isInRegion) this.Cursor = Cursors.Default;switch (DrawModel){case ShapeOperateMode.None: break;case ShapeOperateMode.Move:_currentShape.Move(e.Location);break;case ShapeOperateMode.Resize:_currentShape.Resize(e.Location);break;case ShapeOperateMode.Rotote:_currentShape.Rotate(e.Location);break;}Invalidate();
}
#endregion
ROI的绘图逻辑
- 不同形状的ROI绘制方式有所不同。下面是旋转矩形中内置的方法,文章篇幅有限,但案例代码中有做注释,具体的可自行下载代码查看。
总结
- 文章介绍了如何使用 C# 实现ROI的绘制和管理功能,通过继承ShapeBase类在图像窗体控件中实现ROI的统一管理。通过鼠标事件进行ROI数据变更的交互。
- 功能的实现参考了海康VM的ROI绘制功能,旋转矩形、圆形。数据变更的方法参考了一下Halcon的通过注册事件方法回调结果。
- 通过案例可以更深入学习了解GDI绘图功能的运用,委托、事件的运用;少许的交互逻辑以及一些开发思维。
- 项目有什么缺点?如果有运行项目,缺点是显而易见的。旋转矩形有点问题,旋转后的拖动调整大小位置变换不是基于对角点,视觉效果不佳。
- 回调的数据是没有转换的,正确的处理方法应该是,基于图像将数据转化成对应的图像坐标,但是,目前案例中没有这么做,应该会在后面几篇文章实现。
最后
- 项目源码:gitee.com/incodenotes/csharp-control/tree/master
- 也可以关注微信公众号 [编程笔记in] ,一起交流学习!