【C#】Winform窗体任意控件绘制形状图形(实现添加、删除、选中、移动、缩放功能)

  • 前言

    • 本文描述了如何使用Winform窗体中的控件,绘制形状(如:矩形),实现添加、删除、选中、移动、缩放。点击按钮添加矩形。
    • WinForms (Windows Forms) 是微软.NET框架中的一个图形用户界面(GUI)类库,用于开发Windows桌面应用程序。
    • 在创建窗体后可以通过拖拽方式创建各种控件(如文本框按钮等)。
  • 开发环境

    • 操作系统:Win11
    • 编程软件:Visual Studio 2022
    • 其他相关:.Net Framework 4.6.0
  • 文章内容

    • 描述如何使用WinForm实现绘制形状图形,可以使用在任意Control。
    • 1、绘制矩形、不透明、透明、半透明。
    • 2、按住拖动、点击选中、滚轮缩放。
    • 3、传入控件对象作为参数即可在该控件中绘制。
  • 演示案例

    • 运行效果1

在这里插入图片描述

  • 运行效果2

在这里插入图片描述

  • 代码讲解

    • Shape类

    • 创建形状的基本属性:

      • ID(方便选中)
      • 颜色、线宽、透明度(alpha)
      • 矩形手柄(调整大小)、选中状态。
    • 创建形状的基本方法(抽象方法):

      • 1、判断点是否在手柄上;
      • 2、判断显示手柄方向类型;
      • 3、绘制矩形;
      • 4、绘制手柄。
    public abstract class Shape
     {
         #region 字段、属性
         private int _id = -1;
         private Color _shapeColor = Color.Blue;   //填充颜色
         private Color _handleColor = Color.Blue; //边框颜色
         private float _borderWidth = 0;            //边缘宽度
         private int _alpha = 50;                 // 透明度            
         private int _handleSize = 8;             // 手柄的视觉大小
         private int _handleOutSize = 16;         // 手柄外区域大小(比视觉大)
         private bool _isSelected = true;         //选中状态
         private static int _nextId = 0;
         public int ID { get => _id; private set => _id = value; }
         public Color ShapeColor { get => _shapeColor; set => _shapeColor = value; }
         public Color HandleColor { get => _handleColor; set => _handleColor = value; }
         public float BorderWidth { get => _borderWidth; set => _borderWidth = value; }
         public int Alpha { get => _alpha; set => _alpha = value; }
         public int HandleSize
         {
             get => _handleSize;
             set
             {
                 if (_handleSize != value)
                 {
                     _handleSize = value;
                     HandleOutSize = value * 2;
                 }
             }
         }
         public int HandleOutSize { 
             get => _handleOutSize; 
             set
             {
                 if (_handleOutSize != value)
                 {
                     _handleOutSize = value;
                     _handleSize = value / 2;
                 }
             }
         }
         public bool IsSelected { get => _isSelected; set => _isSelected = value; }
         #endregion
         #region 构造函数
         protected Shape()
         {
             //获取不重复的ID,当前程序运行生效。
             ID = Interlocked.Increment(ref _nextId);
         }
         #endregion
         #region 抽象方法
         /// <summary>
         /// 点是否在手柄上
         /// </summary>
         public abstract bool IsInHandle(Point point, int x, int y);
         /// <summary>
         /// 获取手柄类型
         /// </summary>
         public abstract HandleType GetHandleType(Point point);
         /// <summary>
         /// 绘制手柄
         /// </summary>
         protected abstract void DrawHandle(Graphics graphics, int x, int y);
         /// <summary>
         /// 绘制形状和手柄
         /// </summary>
         public abstract void DrawShapeWithHandles(Graphics graphics);
         #endregion
     }
    
  • URectangle 类

    • 继承Shape类,方便统一形状。
  • 创建矩形的基本属性:

    • 矩形的基本属性参数(X,Y,Width,Height)。
    • Rectangle 实际显示的矩形,RectangleF :用于缩放转换矩形
  • 重写方法:

    • 重写形状方法,实现具体的绘制图形功能。
    public class URectangle: Shape
    {
        #region 字段|属性
        private Rectangle rectangle;
        public Rectangle Rectangle 
        { 
            get => rectangle; 
            set => rectangle = value; 
        }
        private RectangleF rectangleF;
        public RectangleF RectangleF 
        { 
            get => rectangleF; 
            set => rectangleF = value; 
        }
        public int X { 
            get { return rectangle.X; }
            set
            {
                if (rectangle != null) rectangle.X = value;
            }
        }
        public int Y {
            get { return rectangle.Y; }
            set
            {
                if (rectangle != null) rectangle.Y = value;
            }
        }
        public Point Point
        {
            get => new Point(X, Y);
            set
            {
                X = value.X;
                Y = value.Y;
            }
        }
        public int Width {
            get => rectangle.Width;
            set
            {
                if (rectangle!=null)
                {
                    rectangle.Width = value;
                }
            }
        }
        public int Height { 
            get => rectangle.Height;
            set
            {
                if (rectangle != null)
                {
                    rectangle.Height = value;
                }
            }
        }
        public Size Location
        {
            get => new Size(Width, Width);
            set
            {
                Width = value.Width;
                Width = value.Width;
            }
        }
        #endregion
        #region 构造函数
        public URectangle():base()
        {
        }
        public URectangle(Rectangle rectangle)
        {
            this.rectangle = rectangle;
        }
        #endregion
        #region 绘制矩形相关
        /// <summary>
        /// 绘制矩形和手柄(选中时绘制):
        /// </summary>
        public override void DrawShapeWithHandles(Graphics graphics)
        {
            using (var brush = new SolidBrush(Color.FromArgb(Alpha, ShapeColor)))
            {
                graphics.FillRectangle(brush, rectangle);
            }
            Pen pen = new Pen(ShapeColor, BorderWidth);
            graphics.DrawRectangle(pen, rectangle);
            if (IsSelected)
            {
                DrawHandle(graphics, rectangle.Left, rectangle.Top);       // 左上角
                DrawHandle(graphics, rectangle.Right, rectangle.Top);      // 右上角
                DrawHandle(graphics, rectangle.Left, rectangle.Bottom);    // 左下角
                DrawHandle(graphics, rectangle.Right, rectangle.Bottom);   // 右下角
            }
        }
        /// <summary>
        /// 绘制手柄:
        /// </summary>
        protected override void DrawHandle(Graphics graphics, int x, int y)
        {
            Rectangle handleRect = new Rectangle( x - HandleSize / 2,y - HandleSize / 2,HandleSize,HandleSize);
            using (var brush = new SolidBrush(Color.FromArgb(Alpha, HandleColor)))
            {
                graphics.FillRectangle(brush, handleRect);
            }
            Pen pen = new Pen(HandleColor, BorderWidth);
            graphics.DrawRectangle(pen, handleRect);
        }
        /// <summary>
        /// 获取要显示的手柄类型:根据指定点,判断当前矩形应该使用什么类型的手柄。
        /// </summary>
        public override HandleType GetHandleType(Point point)
        {
            if (IsInHandle(point, rectangle.Left, rectangle.Top)) 
                return HandleType.TopLeft;
            if (IsInHandle(point, rectangle.Right, rectangle.Top)) 
                return HandleType.TopRight;
            if (IsInHandle(point, rectangle.Left, rectangle.Bottom)) 
                return HandleType.BottomLeft;
            if (IsInHandle(point, rectangle.Right, rectangle.Bottom)) 
                return HandleType.BottomRight;
            return HandleType.None;
        }
        /// <summary>
        /// 指定点是否在手柄范围内
        /// </summary>
        public override bool IsInHandle(Point point, int x, int y)
        {
            Rectangle handleRect = new Rectangle(x - HandleOutSize / 2,y - HandleOutSize / 2,HandleOutSize,HandleOutSize);
            return handleRect.Contains(point);
        }
        #endregion
    }
    
  • ShapeModule 类 -

    • 创建URectangle 类对象只能创建基本的形状,具体还需要将形状图形绘制到控件中,下面的模块就是如何实现该绘制形状到控件容器中。

    • 通过创建事件、并给容器绑定事件,然后根据事件操作判断实现功能:

      • Container_MouseDown :判断执行绘制、选中
      • Container_MouseMove :判断执行拖动、调整大小
      • Container_MouseUp :判断执行绘制
      • Container_Paint :判断执行绘制形状
      • Container_MouseWheel:判断执行拖放
    public class ShapeModule
     {
         #region 字段 | 属性
         private float scaleX = 1.0f;                        //缩放X
         private float scaleY = 1.0f;                        //缩放Y
         private bool isScaled = false;                      //是否缩放
         private int selectedIndex = -1;                     //选择索引
         private DrawMode currentMode = DrawMode.None;       //当前模式
         private HandleType handleType = HandleType.None;    //手柄类型
         private Point startPoint;                           //起始点
         private Control container;                          //形状显示容器
         private URectangle currentRect;                     //当前矩形
         private URectangle displayRect;                     //选择的矩形
         private ContextMenuStrip rightKeyMenuStrip;         //右键菜单
         private Dictionary<int, Shape> _shapeDictionary;    //形状存储字典
         public float ScaleX
         {
             get => scaleX;
             set
             {
                 scaleX = value;
                 UpdateDisplayShape();
             }
         }
         public float ScaleY
         {
             get => scaleY;
             set
             {
                 scaleY = value;
                 UpdateDisplayShape();
             }
         }
         public DrawMode CurrentMode
         {
             get => currentMode;
             set => currentMode = value;
         }
         public Dictionary<int, Shape> ShapeDictionary { get => _shapeDictionary;private set => _shapeDictionary = value; }
         #endregion
         #region 构造函数
         public ShapeModule(Control container)
         {
             Initialize(container);
         }
         #endregion
         #region 初始化
         private void Initialize(Control container)
         {
             this.container = container;
             ShapeDictionary = new Dictionary<int, Shape>();
             InitializeContainer();
             InitializeRightKeyMenu();
         }
         #region 初始化容器
         private void InitializeContainer()
         {
             container.MouseDown += Container_MouseDown;
             container.MouseUp += Container_MouseUp;
             container.MouseMove += Container_MouseMove;
             container.MouseUp += Container_MouseUp;
             container.MouseWheel += Container_MouseWheel;
             container.Paint += Container_Paint;
         }
         #endregion
         #region 右键菜单功能
         private void InitializeRightKeyMenu()
         {
             // 创建菜单项
             rightKeyMenuStrip = new ContextMenuStrip();
             var copyItem = new ToolStripMenuItem("复制");
             copyItem.Click += (s, e) => CopyAction();
             var deleteItem = new ToolStripMenuItem("删除");
             deleteItem.Click += (s, e) => DeleteAction();
             // 创建右键菜单
             rightKeyMenuStrip.Items.Add(copyItem);
             rightKeyMenuStrip.Items.Add(deleteItem);
         }
         private void CopyAction()
         {
         }
         private void DeleteAction()
         {
             ShapeDictionary.Remove(selectedIndex);
             selectedIndex = -1;
             container.Invalidate();
         }
         #endregion
         #endregion
         #region 形状显示相关:缩放、显示
         /// <summary>
         /// 更新显示形状
         /// </summary>
         private void UpdateDisplayShape()
         {
             foreach (var shape in ShapeDictionary.Values)
             {
                 if (shape is URectangle rectangle)
                 {
                     rectangle.Rectangle = new Rectangle(
                        (int)(rectangle.RectangleF.X * scaleX),
                        (int)(rectangle.RectangleF.Y * scaleY),
                        (int)(rectangle.RectangleF.Width * scaleX),
                        (int)(rectangle.RectangleF.Height * scaleY));
                 }
             }
             container.Invalidate();
         }
         /// <summary>
         /// 缩小
         /// </summary>
         private RectangleF ScaleDown(Rectangle rect)
         {
             return new RectangleF(
                 rect.X / scaleX,
                 rect.Y / scaleY,
                 rect.Width / scaleX,
                 rect.Height / scaleY);
         }
         /// <summary>
         /// 放大
         /// </summary>
         private Rectangle ScaleUp(RectangleF rect)
         {
             return new Rectangle(
                 (int)(rect.X * scaleX),
                 (int)(rect.Y * scaleY),
                 (int)(rect.Width * scaleX),
                 (int)(rect.Height * scaleY));
         }
         /// <summary>
         /// 放大
         /// </summary>
         private void ZoomIn()
         {
             ScaleX += 0.1F;
             ScaleY += 0.1F;
         }
         /// <summary>
         /// 缩小
         /// </summary>
         private void ZoomOut()
         {
             ScaleX -= 0.1F;
             ScaleY -= 0.1F;
         }
         #endregion
         #region 事件
         private void Container_MouseDown(object sender, MouseEventArgs e)
         {
             if (e.Button == MouseButtons.Left)
             {
                 // 形状绘制
                 if (CurrentMode == DrawMode.Drawing)
                 {
                     currentRect = new URectangle();
                     startPoint = e.Location;
                     currentRect.Rectangle = new Rectangle(e.Location, Size.Empty);
                 }
                 // 形状移动
                 else if (CurrentMode == DrawMode.Moving)
                 {
                     int dx = e.X - startPoint.X;
                     int dy = e.Y - startPoint.Y;
                     displayRect.X += dx;
                     displayRect.Y += dy;
                     displayRect.RectangleF = ScaleDown(displayRect.Rectangle);
                     startPoint = e.Location;
                 }
                 // 形状调整大小
                 else if (CurrentMode == DrawMode.Resizing)
                 {
                     Rectangle rect = displayRect.Rectangle;
                     switch (handleType)
                     {
                         case HandleType.TopLeft:
                             rect.X = e.X;
                             rect.Y = e.Y;
                             rect.Width = displayRect.Rectangle.Right - e.X;
                             rect.Height = displayRect.Rectangle.Bottom - e.Y;
                             break;
                         case HandleType.TopRight:
                             rect.Y = e.Y;
                             rect.Width = e.X - displayRect.Rectangle.Left;
                             rect.Height = displayRect.Rectangle.Bottom - e.Y;
                             break;
                         case HandleType.BottomLeft:
                             rect.X = e.X;
                             rect.Width = displayRect.Rectangle.Right - e.X;
                             rect.Height = e.Y - displayRect.Rectangle.Top;
                             break;
                         case HandleType.BottomRight:
                             rect.Width = e.X - displayRect.Rectangle.Left;
                             rect.Height = e.Y - displayRect.Rectangle.Top;
                             break;
                     }
                     // 确保宽度和高度不为负
                     if (rect.Width > 0 && rect.Height > 0)
                     {
                         displayRect.RectangleF = ScaleDown(rect);
                         displayRect.Rectangle = rect;
                     }
                 }
                 // 形状判断选择
                 else
                 {
                     selectedIndex = -1;
                     foreach (var shape in ShapeDictionary.Values)
                     {
                         if (shape is URectangle uRectangle)
                         {
                             if (uRectangle.Rectangle.Contains(e.Location)
                                 || uRectangle.GetHandleType(e.Location) != HandleType.None)
                             {
                                 selectedIndex = uRectangle.ID;
                                 displayRect = uRectangle;
                                 startPoint = e.Location;
                                 handleType = uRectangle.GetHandleType(e.Location);
                                 CurrentMode = handleType != HandleType.None ? DrawMode.Resizing : DrawMode.Moving;
                                 break;
                             }
                         }
                     }
                 }
             }
             //右键菜单
             if (e.Button == MouseButtons.Right)
             {
                 if (selectedIndex != -1)
                 {
                     rightKeyMenuStrip.Show(container, e.Location);
                 }
             }
             container.Invalidate();
         }
         private void Container_MouseMove(object sender, MouseEventArgs e)
         {
             if (e.Button == MouseButtons.Left)
             {
                 if (CurrentMode == DrawMode.Drawing)
                 {
                     int x = Math.Min(startPoint.X, e.X);
                     int y = Math.Min(startPoint.Y, e.Y);
                     int width = Math.Abs(startPoint.X - e.X);
                     int height = Math.Abs(startPoint.Y - e.Y);
                     currentRect.Rectangle = new Rectangle(x, y, width, height);
                 }
                 else if (CurrentMode == DrawMode.Moving && selectedIndex >= 0)
                 {
                     int dx = e.X - startPoint.X;
                     int dy = e.Y - startPoint.Y;
                     displayRect.X += dx;
                     displayRect.Y += dy;
                     ShapeDictionary[selectedIndex] = displayRect;
                     displayRect.RectangleF = ScaleDown(displayRect.Rectangle);
                     startPoint = e.Location;
                 }
                 else if (CurrentMode == DrawMode.Resizing && selectedIndex >= 0)
                 {
                     Rectangle rect = displayRect.Rectangle;
                     switch (handleType)
                     {
                         case HandleType.TopLeft:
                             rect.X = e.X;
                             rect.Y = e.Y;
                             rect.Width = displayRect.Rectangle.Right - e.X;
                             rect.Height = displayRect.Rectangle.Bottom - e.Y;
                             break;
                         case HandleType.TopRight:
                             rect.Y = e.Y;
                             rect.Width = e.X - displayRect.Rectangle.Left;
                             rect.Height = displayRect.Rectangle.Bottom - e.Y;
                             break;
                         case HandleType.BottomLeft:
                             rect.X = e.X;
                             rect.Width = displayRect.Rectangle.Right - e.X;
                             rect.Height = e.Y - displayRect.Rectangle.Top;
                             break;
                         case HandleType.BottomRight:
                             rect.Width = e.X - displayRect.Rectangle.Left;
                             rect.Height = e.Y - displayRect.Rectangle.Top;
                             break;
                     }
                     if (rect.Width > 0 && rect.Height > 0)
                     {
                         displayRect.Rectangle = rect;
                         displayRect.RectangleF = ScaleDown(rect);
                         ShapeDictionary[selectedIndex] = displayRect;
                     }
                 }
             }
             else
             {
                 if (selectedIndex >= 0)
                 {
                     handleType = displayRect.GetHandleType(e.Location);
                     switch (handleType)
                     {
                         case HandleType.TopLeft:
                         case HandleType.BottomRight:
                             container.Cursor = Cursors.SizeNWSE;
                             break;
                         case HandleType.TopRight:
                         case HandleType.BottomLeft:
                             container.Cursor = Cursors.SizeNESW;
                             break;
                         default:
                             container.Cursor = displayRect.Rectangle.Contains(e.Location) ?
                                 Cursors.SizeAll : Cursors.Default;
                             break;
                     }
                 }
                 else
                 {
                     container.Cursor = Cursors.Default;
                 }
             }
             container.Invalidate();
         }
         private void Container_MouseUp(object sender, MouseEventArgs e)
         {
             if (e.Button == MouseButtons.Left)
             {
                 if (CurrentMode == DrawMode.Drawing && currentRect.Width > 0 && currentRect.Height > 0)
                 {
                     currentRect.RectangleF = ScaleDown(currentRect.Rectangle);
                     ShapeDictionary.Add(currentRect.ID, currentRect);
                     selectedIndex = currentRect.ID;
                     displayRect = currentRect;
                     startPoint = e.Location;
                     handleType = currentRect.GetHandleType(e.Location);
                     CurrentMode = handleType != HandleType.None ? DrawMode.Resizing : DrawMode.Moving;
                 }
                 CurrentMode = DrawMode.None;
                 container.Invalidate();
             }
         }
         private void Container_Paint(object sender, PaintEventArgs e)
         {
             foreach (var shape in ShapeDictionary.Values)
             {
                 if (shape.ID == selectedIndex)
                 {
                     shape.IsSelected = true;
                     shape.ShapeColor = Color.Red;
                     shape.HandleColor = Color.White;
                 }
                 else
                 {
                     shape.IsSelected = false;
                     shape.ShapeColor = Color.Blue;
                     shape.HandleColor = Color.White;
                 }
                 shape.DrawShapeWithHandles(e.Graphics);
             }
             if (currentRect == null) return;
             if (CurrentMode == DrawMode.Drawing)
             {
                 using (var brush = new SolidBrush(Color.FromArgb(50, 0, 0, 255)))
                 {
                     e.Graphics.FillRectangle(brush, currentRect.Rectangle);
                 }
                 e.Graphics.DrawRectangle(Pens.Blue, currentRect.Rectangle);
             }
         }
         private void Container_MouseWheel(object sender, MouseEventArgs e)
         {
             if (e.Delta > 0) ZoomOut();
             else if (e.Delta < 0) ZoomIn();
         }
         #endregion
     }
    
  • 用到的枚举

    
    /// <summary>
    /// 绘制模式
    /// </summary>
    public enum DrawMode
    {
        None,
        Drawing,
        Moving,
        Resizing,
    }
    /// <summary>
    /// 手柄类型
    /// </summary>
    public enum HandleType
    {
        None,
        TopLeft,
        TopRight,
        BottomLeft,
        BottomRight
    }
    
  • 用户界面代码

    using CustomControls;
    using Shapes;
    using System;
    using System.Drawing;
    namespace CS学习之绘制形状
    {
        public partial class MainForm : WinFormBase
        {
            ShapeModule shapeModule;
            string imageFile = AppDomain.CurrentDomain.BaseDirectory + "source.png";
            public MainForm()
            {
                InitializeComponent();
                this.CenterToParent();
                shapeModule = new ShapeModule(uc_Canvas);
                uc_Canvas.Image = Image.FromFile(imageFile);
            }
            private void btn_Draw_Click(object sender, System.EventArgs e)
            {
                shapeModule.CurrentMode = DrawMode.Drawing;
            }
        }
    }
    
  • 双缓存图像控件

    public class UImageControl : PictureBox
    {
        public UImageControl()
        {
            this.DoubleBuffered = true; 
            SizeMode = PictureBoxSizeMode.StretchImage;
        }
    }
    
  • 用户界面

在这里插入图片描述

  • 用户界面

    • 该用户界面为前几期的内容创建,如想使用可翻阅前面的内容查看代码。
    • 不使用该窗体也可以使用自定义的Form窗体,只需添加按钮,添加任意控件即可,注意设置Dock属性为Fill。
    • 控件最好设置双缓冲,否则会出现界面闪烁。
    • 设计缺陷:当用户自定义控件绘制图像时,如果也创建了下面实现的事件如鼠标按下事件(MouseDown),可能会起冲突。
    DoubleBuffered  = true;  //启用双缓冲
    
  • 结语

    • 使用方法,将自定义控件创建在同一个命名空间下,点击生成无报错后,即可在工具箱中查看选择自定义控件。
    • 拖拽到当前窗体即可。如果是创建自己的类库,并引用这个类库,引用时可能得报错原因,目标框架不同。
    • 公众号:编程笔记in
  • 最后

    • 如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!
    • 如有疑问,欢迎评论区留言。
    • 也可以关注微信公众号 [编程笔记in] 社区一起交流心得!
    • 项目源码gitee.com/li503560604/cshape-demos
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程笔记in

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值