1. 矩形框控件效果如何?
- 上下左右等8点可以拉伸
- 鼠标滑轮支持缩放,矩形框边框等比例缩放
- 选中矩形框左右拖拽
- 返回矩形框区域对应的图片的X,Y坐标
- 可同时支持多个矩形框
2. 矩形框使用方式?
//矩形框控件添加背景图片 rockRectControl.BackImage = bitmap; //声明一个矩形框,传入左上角和右下角坐标 RockRectangle rect = new RockRectangle(); var p1 = item.DistinguishRegion.LeftTopCorner; var p2 = item.DistinguishRegion.RightBottomCorner; rect.Rectangle = Rectangle.FromLTRB((int)p1.X, (int)p1.Y, (int)p2.X, (int)p2.Y); //把矩形框添加到矩形框控件中,可以添加多个矩形 rockRectControl.RockRectangles.Add(rect);
//找到矩形控件中某一个矩形框 Rectangle r = rockRectControl.RockRectangles[i].Rectangle; //直接读取即可 var rp = new RockRegion(); rp.LeftTopCorner.X = r.X; rp.LeftTopCorner.Y = r.Y; rp.RightBottomCorner.X = r.Right; rp.RightBottomCorner.Y = r.Bottom;
3. 矩形框控件源码?
using System.Drawing; namespace NcModule.Tools; [Serializable] public class RockRectangle { private List<LittleRectangle> littleRectangles = new List<LittleRectangle>(); public Rectangle Rectangle { set; get; } internal List<LittleRectangle> GetLittleRectangles() { littleRectangles.Clear(); littleRectangles.Add(new LittleRectangle(Rectangle, PosSizableRect.LeftUp)); littleRectangles.Add(new LittleRectangle(Rectangle, PosSizableRect.LeftMiddle)); littleRectangles.Add(new LittleRectangle(Rectangle, PosSizableRect.LeftBottom)); littleRectangles.Add(new LittleRectangle(Rectangle, PosSizableRect.BottomMiddle)); littleRectangles.Add(new LittleRectangle(Rectangle, PosSizableRect.RightUp)); littleRectangles.Add(new LittleRectangle(Rectangle, PosSizableRect.RightBottom)); littleRectangles.Add(new LittleRectangle(Rectangle, PosSizableRect.RightMiddle)); littleRectangles.Add(new LittleRectangle(Rectangle, PosSizableRect.UpMiddle)); return littleRectangles; } public double RotationAngle { set; get; } } internal class LittleRectangle { //小矩形的宽度 private int rectangleWidth = 8; /// <summary> /// 矩形放大的倍数 /// </summary> public static double Enlarge = 1; /// <summary> /// 小矩形的位置 /// </summary> public PosSizableRect Location { set; get; } public Rectangle Rectangle { set; get; } public LittleRectangle(Rectangle rect, PosSizableRect location) { this.Location = location; switch (location) { case PosSizableRect.LeftUp: this.Rectangle = createRectSizableNode(rect.X, rect.Y); break; case PosSizableRect.LeftMiddle: this.Rectangle = createRectSizableNode(rect.X, rect.Y + +rect.Height / 2); break; case PosSizableRect.LeftBottom: this.Rectangle = createRectSizableNode(rect.X, rect.Y + rect.Height); break; case PosSizableRect.BottomMiddle: this.Rectangle = createRectSizableNode(rect.X + rect.Width / 2, rect.Y + rect.Height); break; case PosSizableRect.RightUp: this.Rectangle = createRectSizableNode(rect.X + rect.Width, rect.Y); break; case PosSizableRect.RightBottom: this.Rectangle = createRectSizableNode(rect.X + rect.Width, rect.Y + rect.Height); break; case PosSizableRect.RightMiddle: this.Rectangle = createRectSizableNode(rect.X + rect.Width, rect.Y + rect.Height / 2); break; case PosSizableRect.UpMiddle: this.Rectangle = createRectSizableNode(rect.X + rect.Width / 2, rect.Y); break; default: this.Rectangle = new Rectangle(); break; } } private Rectangle createRectSizableNode(int x, int y) { int rectWidth = (int)(rectangleWidth * Enlarge); if (rectWidth < rectangleWidth) { Enlarge = 1; rectWidth = rectangleWidth; } return new Rectangle(x - rectWidth / 2, y - rectWidth / 2, rectWidth, rectWidth); } } internal enum PosSizableRect { UpMiddle, LeftMiddle, LeftBottom, LeftUp, RightUp, RightMiddle, RightBottom, BottomMiddle, None };
using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Windows.Forms; namespace NcModule.Tools; public partial class RockRectControl : UserControl { private Color borderColor = Color.Green; private float borderWidth = 2; private float defaultFontSize = 16; private List<RockRectangle> rockRectangles = new List<RockRectangle>(); //是否显示序号 private bool isPrintNum = true; private Font font = new Font("宋体", 16, FontStyle.Bold); //背景图片 private Image backImage = default!; //图片有效区域 private Rectangle effectiveRect = default(Rectangle); //缩放比例,用double多次运算后会失真,故用百分比 private int zoomScale = 100;//图片本身的缩放比例 private int oldZoomScale = 100; private int zoomMinScale = 60; private int zoomMaxScale = 500; private int stepScale = 20;//每次缩放比例 private Bitmap cloneBackImage = default!; private double imageScale;//真实图片与显示是的缩放比例 private Point realImageCorePoint = new Point();//真实图片的中心点坐标偏移量,当放大或拖拽 时中心点发生变更 private Point wheelPoint = new Point();//滚动时的坐标 private bool zoomScaleIsUpdate = true; public Image BackImage { set { this.backImage = value; if (this.backImage != null) { //黑白图,故格式用Format16bppRgb555,可以降低内存 cloneBackImage = new Bitmap(this.backImage.Width, this.backImage.Height, PixelFormat.Format16bppRgb555); } } get { return this.backImage; } } /// <summary> /// 矩形框的颜色 /// </summary> public Color BorderColor { set { this.borderColor = value; } get { return this.borderColor; } } /// <summary> /// 矩形框边框的粗细 /// </summary> public float BorderWidth { set { this.borderWidth = value; } get { return this.borderWidth; } } public List<RockRectangle> RockRectangles { get { return this.rockRectangles; } } public RockRectControl() { InitializeComponent(); this.init(); } private void init() { //双缓冲 this.DoubleBuffered = true; } private void setFitImageRect() { if (this.cloneBackImage == null) { return; } double imageAspect = this.cloneBackImage.Width * 1.0 / this.cloneBackImage.Height; double controlAspect = this.Width * 1.0 / this.Height; //以高为主 if (imageAspect < controlAspect) { double imageHeight = this.Height; double imageWidth = imageHeight * imageAspect; int x = (int)((this.Width - imageWidth) / 2); this.effectiveRect = new Rectangle(x, 0, (int)imageWidth, (int)imageHeight); } else { //以宽为主 double imageWidth = this.Width; double imageHeight = imageWidth / imageAspect; int y = (int)((this.Height - imageHeight) / 2); this.effectiveRect = new Rectangle(0, y, (int)imageWidth, (int)imageHeight); } this.imageScale = this.cloneBackImage.Width * 1.0 / this.effectiveRect.Width; //放大的最大值,只能放大到图片本来的大小 this.zoomMaxScale = (int)Math.Round(imageScale * 100); } //记录移动前鼠标的位置 private int oldCursorX, oldCursorY; private int selectRectIndex = -1; private PosSizableRect selectLocation = PosSizableRect.None; protected override void OnMouseDown(MouseEventArgs e) { if (e.Button == MouseButtons.Left) { if (this.effectiveRect.Contains(e.Location)) { Point imageP = this.localPoint2ImagePoint(e.Location); this.oldCursorX = imageP.X; this.oldCursorY = imageP.Y; this.changeCursor(imageP, true); if (selectLocation != PosSizableRect.None) { return; } //判断当前位置是在哪个矩形内 foreach (var item in this.RockRectangles) { if (this.isInRect(imageP, item.Rectangle, item.RotationAngle)) { this.selectRectIndex = this.RockRectangles.IndexOf(item); return; } } } } selectRectIndex = -1; } protected override void OnMouseUp(MouseEventArgs e) { selectRectIndex = -1; selectLocation = PosSizableRect.None; this.Invalidate(); } protected override void OnMouseMove(MouseEventArgs le) { if (le.Button == MouseButtons.Left) { if (this.selectRectIndex != -1) { Rectangle rect = this.RockRectangles[this.selectRectIndex].Rectangle; Point e = this.localPoint2ImagePoint(le.Location); switch (selectLocation) { case PosSizableRect.LeftUp: rect.X += e.X - oldCursorX; rect.Width -= e.X - oldCursorX; rect.Y += e.Y - oldCursorY; rect.Height -= e.Y - oldCursorY; break; case PosSizableRect.LeftMiddle: rect.X += e.X - oldCursorX; rect.Width -= e.X - oldCursorX; break; case PosSizableRect.LeftBottom: rect.Width -= e.X - oldCursorX; rect.X += e.X - oldCursorX; rect.Height += e.Y - oldCursorY; break; case PosSizableRect.BottomMiddle: rect.Height += e.Y - oldCursorY; break; case PosSizableRect.RightUp: rect.Width += e.X - oldCursorX; rect.Y += e.Y - oldCursorY; rect.Height -= e.Y - oldCursorY; break; case PosSizableRect.RightBottom: rect.Width += e.X - oldCursorX; rect.Height += e.Y - oldCursorY; break; case PosSizableRect.RightMiddle: rect.Width += e.X - oldCursorX; break; case PosSizableRect.UpMiddle: rect.Y += e.Y - oldCursorY; rect.Height -= e.Y - oldCursorY; break; default: rect.X = rect.X + e.X - this.oldCursorX; rect.Y = rect.Y + e.Y - this.oldCursorY; break; } this.RockRectangles[this.selectRectIndex].Rectangle = rect; this.oldCursorX = e.X; this.oldCursorY = e.Y; Invalidate(); } } else { if (this.effectiveRect.Contains(le.Location)) { this.changeCursor(this.localPoint2ImagePoint(le.Location)); } else { this.Cursor = Cursors.Default; } } } protected override void OnMouseWheel(MouseEventArgs e) { this.wheelPoint = this.localPoint2ImagePoint(e.Location); if (e.Delta > 0)//上滚放大 { if (this.zoomScale < this.zoomMaxScale) { this.stepScale = Math.Abs(this.stepScale); this.zoomScale += this.stepScale; } } else { //下滚缩小 if (this.zoomScale > this.zoomMinScale) { this.stepScale = -Math.Abs(this.stepScale); this.zoomScale += this.stepScale; } } this.Invalidate(); } protected override void OnSizeChanged(EventArgs e) { this.Invalidate(); } protected override void OnPaint(PaintEventArgs pe) { //背景图片存在才绘制 if (this.cloneBackImage != null) { this.setFitImageRect(); //把矩形画在背景图片上 this.paintRect(); //把图片绘制到界面上 this.paintImageToControl(pe.Graphics); } } //在背景图片上画框 private void paintRect() { var g = Graphics.FromImage(cloneBackImage); //画背景图 g.DrawImage(this.backImage, 0, 0, cloneBackImage.Width, cloneBackImage.Height); //画的线平滑 //g.InterpolationMode = InterpolationMode.Low; //设置高质量,低速度呈现平滑程度 //g.SmoothingMode = SmoothingMode.HighSpeed; g.CompositingQuality = CompositingQuality.AssumeLinear; //在图像上矩形 using (var path = new GraphicsPath()) { foreach (var item in this.RockRectangles) { //动态加粗线条 double enlarge = this.imageScale * 100 / this.zoomScale; float nBorderWidth = (float)(this.borderWidth * enlarge); if (nBorderWidth < this.borderWidth) { nBorderWidth = this.borderWidth; } LittleRectangle.Enlarge = enlarge; this.font = new Font("宋体", (float)(this.defaultFontSize * enlarge), FontStyle.Bold); path.Reset(); this.getPath(path, item.Rectangle, item.RotationAngle); g.DrawPath(new Pen(this.borderColor, nBorderWidth), path); //写序号 if (this.isPrintNum) { string num = (this.RockRectangles.IndexOf(item) + 1).ToString(); g.DrawString(num, this.font, new SolidBrush(this.borderColor), this.getCenter(item.Rectangle)); } //画每个大矩形里面的8个小矩形 //获取8个小矩形 var littleRects = item.GetLittleRectangles(); Rectangle rect = item.Rectangle; Point center = new Point(rect.X + rect.Width / 2, rect.Y + rect.Height / 2); foreach (var littleRect in littleRects) { path.Reset(); this.getPath(path, littleRect.Rectangle, item.RotationAngle, center); g.DrawPath(new Pen(this.borderColor, nBorderWidth), path); } } } g.Dispose(); } //把图片绘制到控件上 private void paintImageToControl(Graphics g) { //设置高质量插值法 g.InterpolationMode = InterpolationMode.High; //设置高质量,低速度呈现平滑程度 g.SmoothingMode = SmoothingMode.HighQuality; g.CompositingQuality = CompositingQuality.GammaCorrected; //获取图片的的区域 int width = (int)(cloneBackImage.Width * 100 / zoomScale); int height = (int)(cloneBackImage.Height * 100 / zoomScale); //此时是以中心点来缩放的,如果以滑轮中心缩放,则需要知道实际图片的width和height的缩放比例 //原理是放大后,鼠标相对于控件坐标不变,鼠标向对于图像坐标也不变 //realImageCorePoint在反复计算时有极少误差,所以当zoomScale不变化是,不更新realImageCorePoint if (zoomScale != 100) { if (this.oldZoomScale != this.zoomScale) { realImageCorePoint.X = (int)Math.Round((this.stepScale * wheelPoint.X + (this.zoomScale - this.stepScale) * realImageCorePoint.X) * 1.0 / this.zoomScale); realImageCorePoint.Y = (int)Math.Round((this.stepScale * wheelPoint.Y + (this.zoomScale - this.stepScale) * realImageCorePoint.Y) * 1.0 / this.zoomScale); this.oldZoomScale = this.zoomScale; zoomScaleIsUpdate = true; } else { //当放大停止后,需要重新刷新一次 if (zoomScaleIsUpdate) { this.Invalidate(); zoomScaleIsUpdate = false; } } } else { realImageCorePoint.X = (int)((cloneBackImage.Width - width) / 2.0); realImageCorePoint.Y = (int)((cloneBackImage.Height - height) / 2.0); } Rectangle srcRect = new Rectangle(realImageCorePoint.X, realImageCorePoint.Y, width, height); g.DrawImage(cloneBackImage, this.effectiveRect, srcRect, GraphicsUnit.Pixel); } //控件中的点与元素图像的点转换 private Point localPoint2ImagePoint(Point p) { p.X = (int)((p.X - (this.Width - this.effectiveRect.Width) / 2.0) * imageScale * 100 / zoomScale) + realImageCorePoint.X; p.Y = (int)((p.Y - (this.Height - this.effectiveRect.Height) / 2.0) * imageScale * 100 / zoomScale) + realImageCorePoint.Y; return p; } private bool isInRect(Point p, Rectangle rect, double angle) { Point centerP = this.getCenter(rect); //获取反旋转后的点 return this.isInRect(p, rect, angle, centerP); } /// <summary> /// 判断某个点是否在矩形内 /// </summary> /// <param name="p"></param> /// <param name="rect"></param> /// <param name="angle"></param> /// <param name="centerP"></param> /// <returns></returns> private bool isInRect(Point p, Rectangle rect, double angle, Point centerP) { //获取反旋转后的点 Point rotateP = this.getRotatePoint(p, -angle, centerP); return rect.Contains(rotateP); } /// <summary> /// 改变鼠标的图标 /// </summary> /// <param name="p"></param> private void changeCursor(Point p, bool updateSelectData = false) { bool isInBigRect = false; foreach (var item in this.RockRectangles) { if (this.isInRect(p, item.Rectangle, item.RotationAngle)) { isInBigRect = true; } foreach (var littleRect in item.GetLittleRectangles()) { //如果图标在小矩形内 if (this.isInRect(p, littleRect.Rectangle, item.RotationAngle, this.getCenter(item.Rectangle))) { this.Cursor = this.getCursor(littleRect.Location); if (updateSelectData) { this.selectRectIndex = this.RockRectangles.IndexOf(item); this.selectLocation = littleRect.Location; } return; } } } if (isInBigRect) { this.Cursor = Cursors.SizeAll; } else { this.Cursor = Cursors.Default; } } private Cursor getCursor(PosSizableRect p) { switch (p) { case PosSizableRect.LeftUp: return Cursors.SizeNWSE; case PosSizableRect.LeftMiddle: return Cursors.SizeWE; case PosSizableRect.LeftBottom: return Cursors.SizeNESW; case PosSizableRect.BottomMiddle: return Cursors.SizeNS; case PosSizableRect.RightUp: return Cursors.SizeNESW; case PosSizableRect.RightBottom: return Cursors.SizeNWSE; case PosSizableRect.RightMiddle: return Cursors.SizeWE; case PosSizableRect.UpMiddle: return Cursors.SizeNS; default: return Cursors.Default; } } //获取矩形中心 private Point getCenter(Rectangle rect) { return new Point(rect.X + rect.Width / 2, rect.Y + rect.Height / 2); } /// <summary> /// 获取矩形旋转后的路径 /// </summary> /// <param name="rectangle"></param> /// <param name="angle"></param> private void getPath(GraphicsPath path, Rectangle rect, double angle) { Point center = this.getCenter(rect); this.getPath(path, rect, angle, center); } private void getPath(GraphicsPath path, Rectangle rect, double angle, Point center) { path.AddRectangle(rect); var a = -angle * (Math.PI / 180); var n1 = (float)Math.Cos(a); var n2 = (float)Math.Sin(a); var n3 = -(float)Math.Sin(a); var n4 = (float)Math.Cos(a); var n5 = (float)(center.X * (1 - Math.Cos(a)) + center.Y * Math.Sin(a)); var n6 = (float)(center.Y * (1 - Math.Cos(a)) - center.X * Math.Sin(a)); Matrix matrix = new Matrix(n1, n2, n3, n4, n5, n6); path.Transform(matrix); } //p1绕center旋转angle角度后点位 private Point getRotatePoint(Point p1, double angle, Point center) { //使用旋转矩阵求值 System.Windows.Media.RotateTransform rotateTransform = new System.Windows.Media.RotateTransform(angle, center.X, center.Y); System.Windows.Point p = new System.Windows.Point(p1.X, p1.Y); System.Windows.Point p2 = rotateTransform.Transform(p); Point result = new Point(); result.X = (int)p2.X; result.Y = (int)p2.Y; return result; } }
4. 矩形框控件不足?
- 目前矩形框控件不支持对背景图片的拖拽(本项目中未涉及此场景,后续可能会增加此功能)
- 目前矩形框控件不支持旋转(源码中有旋转矩形框展示代码,但交互上没有实现,需要人为赋值旋转角度,后续可能会优化)