選擇影象區域矩形框控制元件【原創】

2022-11-10 18:01:19

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. 矩形框控制元件原始碼?

  • 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. 矩形框控制元件不足?

  • 目前矩形框控制元件不支援對背景圖片的拖拽(本專案中未涉及此場景,後續可能會增加此功能)
  • 目前矩形框控制元件不支援旋轉(原始碼中有旋轉矩形框展示程式碼,但互動上沒有實現,需要人為賦值旋轉角度,後續可能會優化)