6

选择图像区域矩形框控件【原创】 - Bonker

 1 year ago
source link: https://www.cnblogs.com/Bonker/p/16877328.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1. 矩形框控件效果如何?

test2.gif
  • 上下左右等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. 矩形框控件源码?

  • RockRectangle源码
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
};
  • RockRectControl源码
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. 矩形框控件不足?

  • 目前矩形框控件不支持对背景图片的拖拽(本项目中未涉及此场景,后续可能会增加此功能)
  • 目前矩形框控件不支持旋转(源码中有旋转矩形框展示代码,但交互上没有实现,需要人为赋值旋转角度,后续可能会优化)

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK