自定义控件(3) 流动条

自定义控件学习(3) 流动条

案例1 简单流动条(可以向上,下,左,右,但是不能倾斜)


流动条思路:

  1. 首先这是一个基本的流动条,基本上可以分为三个部分。左侧,中间和右侧。左侧和右侧部分可以横向或者纵向。
  2. 需要考虑的设计属性
    1. 流动条宽度
    2. 流动颜色:可以修改流动液体颜色
    3. 边缘颜色:管道边缘使用渐变色
    4. 中间颜色
    5. 管道左、上边的朝向:
    6. 管道右、下边的朝向:
    7. 管道样式:
    8. 是否激活液体流动:
    9. 液体流动的速度:
    10. 边线颜色:流动条外部轮廓线
    11. 流动长度:流动液体条的长度
    12. 间隙长度:

先贴上完整代码,后面逐函数进行分析讲解:

using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace FrmMain
{
    /// <summary>
    /// 管道左、右的转向类型
    /// </summary>
    public enum PipeTurnDirection
    {
        Up = 1,
        Down,
        Left,
        Right,
        None
    }

    /// <summary>
    /// 管道的样式,水平还是数值
    /// </summary>
    public enum DirectionStyle
    {
        Horizontal = 1,
        Vertical
    }

    public partial class ZFlowCtl : UserControl
    {

        public ZFlowCtl()
        {
            InitializeComponent();
            this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            this.SetStyle(ControlStyles.DoubleBuffer, true);
            this.SetStyle(ControlStyles.ResizeRedraw, true);
            this.SetStyle(ControlStyles.Selectable, true);
            this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
            this.SetStyle(ControlStyles.UserPaint, true);
            this.mytimer = new Timer();
            mytimer.Interval = 50;
            this.mytimer.Tick += Mytimer_Tick;
        }
   

        private void Mytimer_Tick(object sender, EventArgs e)
        {
            this.startOffset = this.startOffset - this.moveSpeed;

            if (this.startOffset > this.pipeLength + this.gapLength || this.startOffset < (this.pipeLength + this.gapLength) * (-1))
            {
                this.startOffset = 0.0f;
            }

            this.Invalidate();
        }

        private Graphics g;

        private Pen p;

        private float startOffset = 0.0f;

        private Timer mytimer;


        #region Fileds

        private int pipeWidth = 5;

        [Browsable(true), Category("自定义属性"), Description("流动条宽度")]
        public int PipeWidth
        {
            get { return pipeWidth; }
            set
            {
                this.pipeWidth = value;

                base.Invalidate();
            }
        }

        private Color colorCenter = Color.DodgerBlue;

        [Browsable(true), Category("自定义属性"), Description("获取或设置管道控件的流动颜色")]
        public Color ColorCenter
        {
            get
            {
                return this.colorCenter;
            }
            set
            {
                this.colorCenter = value;
                base.Invalidate();
            }
        }

        private Color borderColor = Color.DimGray;

        [Browsable(true), Category("自定义属性"), Description("获取或设置管道边线颜色")]
        public Color BorderColor
        {
            get
            {
                return this.borderColor;
            }
            set
            {
                this.borderColor = value;
                p = new Pen(value, 1.0f);
                base.Invalidate();
            }
        }

        private Color edgeColor = Color.DimGray;

        [Browsable(true), Category("自定义属性"), Description("获取或设置管道边缘颜色")]
        public Color EdgeColor
        {
            get
            {
                return this.edgeColor;
            }
            set
            {
                this.edgeColor = value;
                base.Invalidate();
            }
        }

        private Color centerColor = Color.LightGray;

        [Browsable(true), Category("自定义属性"), Description("获取或设置管道控件的中心颜色")]
        public Color LineCenterColor
        {
            get
            {
                return this.centerColor;
            }
            set
            {
                this.centerColor = value;
                base.Invalidate();
            }
        }


        private PipeTurnDirection pipeTurnLeft = PipeTurnDirection.None;

        [Browsable(true), Category("自定义属性"), Description("左管道的转向类型")]
        public PipeTurnDirection PipeTurnLeft
        {
            get
            {
                return this.pipeTurnLeft;
            }
            set
            {
                this.pipeTurnLeft = value;
                base.Invalidate();
            }
        }

        private PipeTurnDirection pipeTurnRight = PipeTurnDirection.None;

        [Browsable(true), Category("自定义属性"), Description("右管道的转向类型")]
        public PipeTurnDirection PipeTurnRight
        {
            get
            {
                return this.pipeTurnRight;
            }
            set
            {
                this.pipeTurnRight = value;
                base.Invalidate();
            }
        }

        private DirectionStyle pipeLineStyle = DirectionStyle.Horizontal;

        [Browsable(true), Category("自定义属性"), Description("设置管道是横向的还是纵向的")]
        public DirectionStyle PipeLineStyle
        {
            get
            {
                return this.pipeLineStyle;
            }
            set
            {
                this.pipeLineStyle = value;
                base.Invalidate();
            }
        }

        private bool isActive = false;

        [Browsable(true), Category("自定义属性"), DefaultValue(false), Description("获取或设置管道线是否激活液体显示")]
        public bool IsActive
        {
            get
            {
                return this.isActive;
            }
            set
            {
                this.isActive = value;
                this.mytimer.Enabled = value;
                base.Invalidate();
            }
        }


        private float moveSpeed = 0.3f;

        [Browsable(true), Category("自定义属性"), Description("管道线液体流动的速度,0为静止,正数为正向流动,负数为反向流动")]
        public float MoveSpeed
        {
            get
            {
                return this.moveSpeed;
            }
            set
            {
                this.moveSpeed = value;
                base.Invalidate();
            }
        }

        private int pipeLength = 5;

        [Browsable(true), Category("自定义属性"), Description("流动条长度")]
        public int PipeLength
        {
            get
            {
                return this.pipeLength;
            }
            set
            {
                this.pipeLength = value;
                base.Invalidate();
            }
        }

        private int gapLength = 5;

        [Browsable(true), Category("自定义属性"), Description("间隙长度")]
        public int GapLength
        {
            get
            {
                return this.gapLength;
            }
            set
            {
                this.gapLength = value;
                base.Invalidate();
            }
        }


        #endregion

        #region Override

        protected override void OnPaint(PaintEventArgs e)
        {
            g = e.Graphics;

            g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;

            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

            p = new Pen(this.borderColor, 1.0f);

            ColorBlend colorBlend = new ColorBlend();

            //渐变线的比例
            colorBlend.Positions = new float[]
                {
                    0.0f,0.5f,1.0f
                };
            colorBlend.Colors = new Color[]
                {
                    this.edgeColor,this.centerColor,this.edgeColor
                };
            //水平管道
            if (this.pipeLineStyle == DirectionStyle.Horizontal)
            {
                LinearGradientBrush linearGradientBrush = new LinearGradientBrush(new Point(0, 0), new Point(0, this.Height), edgeColor, edgeColor);

                linearGradientBrush.InterpolationColors = colorBlend;
                //绘制左部分
                switch (this.pipeTurnLeft)
                {
                    case PipeTurnDirection.Up:
                        this.PaintEllipse(g, colorBlend, p, new Rectangle(0, this.Height * (-1), this.Height * 2, this.Height * 2), 90.0f, 90.0f);
                        break;
                    case PipeTurnDirection.Down:
                        this.PaintEllipse(g, colorBlend, p, new Rectangle(0, 0, this.Height * 2, this.Height * 2), 180.0f, 90.0f);
                        break;
                    default:
                        this.PaintRectangle(g, linearGradientBrush, p, new Rectangle(0, 0, this.Height, this.Height));
                        break;
                }


                //绘制右部分
                switch (this.pipeTurnRight)
                {
                    case PipeTurnDirection.Up:
                        this.PaintEllipse(g, colorBlend, p, new Rectangle(this.Width - this.Height * 2, this.Height * (-1), this.Height * 2, this.Height * 2), 0.0f, 90.0f);
                        break;
                    case PipeTurnDirection.Down:
                        this.PaintEllipse(g, colorBlend, p, new Rectangle(this.Width - this.Height * 2, 0, this.Height * 2, this.Height * 2), 270.0f, 90.0f);
                        break;
                    default:
                        this.PaintRectangle(g, linearGradientBrush, p, new Rectangle(this.Width - this.Height, 0, this.Height, this.Height));
                        break;
                }

                //绘制中间
                if (this.Width > this.Height * 2)
                {
                    this.PaintRectangle(g, linearGradientBrush, p, new Rectangle(this.Height - 1, 0, this.Width - this.Height * 2 + 2, this.Height));
                }

                if (isActive)
                {
                    GraphicsPath graphicsPath = new GraphicsPath();

                    switch (this.pipeTurnLeft)
                    {
                        case PipeTurnDirection.Up:
                            graphicsPath.AddArc(new Rectangle(this.Height / 2, this.Height / 2 * (-1) - 1, this.Height, this.Height), 180.0f, -90.0f);
                            break;
                        case PipeTurnDirection.Down:
                            graphicsPath.AddArc(new Rectangle(this.Height / 2, this.Height / 2, this.Height, this.Height), 180.0f, 90.0f);
                            break;
                        default:
                            graphicsPath.AddLine(0, this.Height / 2, this.Height, this.Height / 2);
                            break;
                    }

                    if (this.Width > this.Height * 2)
                    {
                        graphicsPath.AddLine(base.Height, base.Height / 2, base.Width - base.Height - 1, base.Height / 2);
                    }

                    switch (this.pipeTurnRight)
                    {
                        case PipeTurnDirection.Up:
                            graphicsPath.AddArc(new Rectangle(base.Width - 1 - base.Height * 3 / 2, -base.Height / 2 - 1, base.Height, base.Height), 90f, -90f);
                            break;
                        case PipeTurnDirection.Down:
                            graphicsPath.AddArc(new Rectangle(base.Width - 1 - base.Height * 3 / 2, base.Height / 2, base.Height, base.Height), 270f, 90f);
                            break;
                        default:
                            graphicsPath.AddLine(base.Width - base.Height, base.Height / 2, base.Width - 1, base.Height / 2);
                            break;
                    }

                    //其实就是画虚线,画虚线的关键在于笔和路径

                    Pen pen = new Pen(this.colorCenter, this.pipeWidth);
                    pen.DashStyle = DashStyle.Custom;
                    pen.DashPattern = new float[]
                        {
                            pipeLength,gapLength
                        };
                    pen.DashOffset = this.startOffset;
                    g.DrawPath(pen, graphicsPath);

                }
            }
            //竖直管道
            else
            {
                LinearGradientBrush linearGradientBrush2 = new LinearGradientBrush(new Point(0, 0), new Point(this.Width, 0), edgeColor, edgeColor);

                linearGradientBrush2.InterpolationColors = colorBlend;
                //绘制上部分
                switch (this.pipeTurnLeft)
                {
                    case PipeTurnDirection.Left:
                        this.PaintEllipse(g, colorBlend, p, new Rectangle(this.Width * (-1), 0, this.Width * 2, this.Width * 2), 270.0f, 90.0f);
                        break;
                    case PipeTurnDirection.Right:
                        this.PaintEllipse(g, colorBlend, p, new Rectangle(0, 0, this.Width * 2, this.Width * 2), 180.0f, 90.0f);
                        break;
                    default:
                        this.PaintRectangle(g, linearGradientBrush2, p, new Rectangle(0, 0, this.Width, this.Width));
                        break;
                }

                //绘制下部分
                switch (this.pipeTurnRight)
                {
                    case PipeTurnDirection.Left:
                        this.PaintEllipse(g, colorBlend, p, new Rectangle(this.Width * (-1), this.Height - this.Width * 2, this.Width * 2, this.Width * 2), 0.0f, 90.0f);
                        break;
                    case PipeTurnDirection.Right:
                        this.PaintEllipse(g, colorBlend, p, new Rectangle(0, this.Height - this.Width * 2, this.Width * 2, this.Width * 2), 90.0f, 90.0f);
                        break;
                    default:
                        this.PaintRectangle(g, linearGradientBrush2, p, new Rectangle(0, this.Height - this.Width, this.Width, this.Width));
                        break;
                }

                //绘制中间
                if (this.Height > this.Width * 2)
                {
                    this.PaintRectangle(g, linearGradientBrush2, p, new Rectangle(0, this.Width - 1, this.Width, this.Height - this.Width * 2 + 2));
                }

                if (isActive)
                {
                    //绘制路径
                    GraphicsPath graphicsPath = new GraphicsPath();

                    switch (this.pipeTurnLeft)
                    {
                        case PipeTurnDirection.Left:
                            graphicsPath.AddArc(new Rectangle(this.Width / 2 * (-1), this.Width / 2 - 1, this.Width, this.Width), 270.0f, 90.0f);
                            break;
                        case PipeTurnDirection.Right:
                            graphicsPath.AddArc(new Rectangle(this.Width / 2, this.Width / 2 - 1, this.Width, this.Width), 270.0f, -90.0f);
                            break;
                        default:
                            graphicsPath.AddLine(this.Width / 2, 0, this.Width / 2, this.Width);
                            break;
                    }

                    if (this.Height > this.Width * 2)
                    {
                        graphicsPath.AddLine(this.Width / 2 - 1, this.Width, this.Width / 2 - 1, this.Height - this.Width - 1);
                    }

                    switch (this.pipeTurnRight)
                    {
                        case PipeTurnDirection.Left:
                            graphicsPath.AddArc(new Rectangle(this.Width / 2 * (-1), this.Height - this.Width / 2 * 3 - 1, this.Width, this.Width), 0f, 90f);
                            break;
                        case PipeTurnDirection.Right:
                            graphicsPath.AddArc(new Rectangle(this.Width / 2, this.Height - this.Width / 2 * 3 - 1, this.Width, this.Width), -180, -90f);
                            break;
                        default:
                            graphicsPath.AddLine(this.Width / 2, this.Height - this.Width, this.Width / 2, this.Height);
                            break;
                    }

                    //其实就是画虚线,画虚线的关键在于笔和路径

                    Pen pen = new Pen(this.colorCenter, this.pipeWidth);
                    pen.DashStyle = DashStyle.Custom;
                    pen.DashPattern = new float[]
                        {
                            pipeLength,gapLength
                        };
                    pen.DashOffset = this.startOffset;
                    g.DrawPath(pen, graphicsPath);

                }
            }

            base.OnPaint(e);
        }

        #endregion

        #region Methods 公用的功能函数

        /// <summary>
        /// 根据外切矩形绘制内部的扇形
        /// </summary>
        /// <param name="g"></param>
        /// <param name="colorBlend"></param>
        /// <param name="p"></param>
        /// <param name="rect"></param>
        /// <param name="startAngle"></param>
        /// <param name="sweepAngle"></param>
        private void PaintEllipse(Graphics g, ColorBlend colorBlend, Pen p, Rectangle rect, float startAngle, float sweepAngle)
        {
            //第一步:创建GraphicsPath
            GraphicsPath graphicsPath = new GraphicsPath();
            graphicsPath.AddEllipse(rect);

            //第二步:PathGradientBrush
            PathGradientBrush pathGradientBrush = new PathGradientBrush(graphicsPath);

            pathGradientBrush.CenterPoint = new Point(rect.X + rect.Width / 2, rect.Y + rect.Height / 2);
            pathGradientBrush.InterpolationColors = colorBlend;

            //第三步:绘制Pipe
            g.FillPie(pathGradientBrush, rect, startAngle, sweepAngle);

            //第四步:绘制边线
            g.DrawArc(p, rect, startAngle, sweepAngle);

        }

        /// <summary>
        /// 根据外切矩形填充渐变色
        /// </summary>
        /// <param name="g"></param>
        /// <param name="brush"></param>
        /// <param name="pen"></param>
        /// <param name="rectangle"></param>
        private void PaintRectangle(Graphics g, Brush brush, Pen pen, Rectangle rectangle)
        {
            //填充矩形
            g.FillRectangle(brush, rectangle);

            switch (this.pipeLineStyle)
            {
                case DirectionStyle.Horizontal:
                    g.DrawLine(pen, rectangle.X, rectangle.Y, rectangle.X + rectangle.Width, rectangle.Y);
                    //g.DrawLine(pen, rectangle.X, rectangle.Y + rectangle.Height - 1, rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height - 1);
                    break;
                case DirectionStyle.Vertical:
                    g.DrawLine(pen, rectangle.X, rectangle.Y, rectangle.X, rectangle.Y + rectangle.Height);
                    // g.DrawLine(pen, rectangle.X + rectangle.Width - 1, rectangle.Y, rectangle.X + rectangle.Width - 1, rectangle.Y + rectangle.Height);
                    break;
                default:
                    break;
            }

        }

        #endregion

    }
}
  1. 第一部分 属性部分。这部分代码,基本就是按照设计要求进行照猫画虎。但是特别注意两点。

    1. 需要立刻生效进行图像重绘的属性,需要使用Invalidate方法

    2. borderColor属性是获取或设置管道边线颜色。在后期可能会使用该颜色作为画笔的颜色。所以需要此时对画笔进行颜色赋值。

  2. 第二部分 图形重绘

    img

    1. 第一个部分 图形消除锯齿化,图形高质量.这个基本常用

      g = e.Graphics;       
      g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
      g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
      g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
      
    2. 第二个部分 管道主体部分的绘制。相关函数如下:

      1. 首先选好渐变色线条
        //管道中间到管道边缘使用 渐变色线条
        ColorBlend colorBlend = new ColorBlend();
        colorBlend.Positions = new float[]{ 0.0f,0.5f,1.0f};
        colorBlend.Colors = new Color[]{this.edgeColor,this.centerColor,this.edgeColor };
      
      
      1. 先判断管道是否为横向

        1. 当为横向以后,调用渐变色刷。LinearGradientBrush传入起始点(水平部分的左上角为原点)和结束点(水平部分的右上角为结束点,所以数值为管道的长度),此处只是设定绘制长度参数,但是实际没有进行绘制

          情况说明:如果没有左右拐头那么水平部分会把拐头的控件都变为水平部分。如果有拐头,那么图像是先绘制长的水平部分,然后再重新绘制拐头部分(有拐头和无拐头的水平长度是不相等的

          img

             LinearGradientBrush linearGradientBrush = new LinearGradientBrush(new Point(0, 0), new Point(0, this.Height), edgeColor, edgeColor);
            linearGradientBrush.InterpolationColors = colorBlend;
          

          img

        2. 绘制左半部分。首先左半部分有三种可能情况。第一种往上拐,第二种往下拐。第三种不拐,直接结束。

            //绘制左部分
                          switch (this.pipeTurnLeft)
                          {
                          case PipeTurnDirection.Up:
                                  this.PaintEllipse(g, colorBlend, p, new Rectangle(0, this.Height * (-1), this.Height * 2, this.Height * 2), 90.0f, 90.0f);break;
                          case PipeTurnDirection.Down:
                                  this.PaintEllipse(g, colorBlend, p, new Rectangle(0, 0, this.Height * 2, this.Height * 2), 180.0f, 90.0f); break;
                           default:
                                  this.PaintRectangle(g, linearGradientBrush, p, new Rectangle(0, 0, this.Height, this.Height)); break;
                          }
          

          首先第一种情况向上拐。PaintEllipse方法用来绘制椭圆,需要传入Graphics实例、渐变颜色、画刷(borderColor设置了颜色)与外切矩形参数。扇形的角度是从90°到180°。

          外切矩形分析如下:首先这个椭圆扇形的部分的圆心是拐头和水平部分交接的中心。外切矩形的起始点就在拐头最左侧的向上一个半径的地方。

          img

          向下拐,分析方法基本相同。

           //绘制右部分
          switch (this.pipeTurnRight)
                          {
                              case PipeTurnDirection.Up:
                                  this.PaintEllipse(g, colorBlend, p, new Rectangle(this.Width - this.Height * 2, this.Height * (-1), this.Height * 2, this.Height * 2), 0.0f, 90.0f);
                                  break;
                              case PipeTurnDirection.Down:
                                  this.PaintEllipse(g, colorBlend, p, new Rectangle(this.Width - this.Height * 2, 0, this.Height * 2, this.Height * 2), 270.0f, 90.0f);
                                  break;
                              default:
                                  this.PaintRectangle(g, linearGradientBrush, p, new Rectangle(this.Width - this.Height, 0, this.Height, this.Height));
                                  break;
                          }               
          

          中间部分的绘制代码如下,稍微会加一个补偿值。基本上设置线条长宽就是从头到尾

          this.PaintRectangle(g, linearGradientBrush, p, new Rectangle(this.Height - 1, 0, this.Width - this.Height * 2 + 2, this.Height));
          
          1. 绘制流动条。首先思考下,流动条的本质就是一条虚线。然后让虚线出现定时的偏移量(使用定时器)

             					首先确认IsActive为true(获取或设置管道线是否激活液体显示)
             					
             					 1. 绘制虚线 DashStyle (设置虚线长度)  DashPattern(设置虚线的样式)DashOffset(偏移量)
             					
             					    ```c#
             					    Pen pen = new Pen(this.colorCenter, this.pipeWidth);
             					    pen.DashStyle = DashStyle.Custom;
             					    pen.DashPattern = new float[]{pipeLength,gapLength };
             					    pen.DashOffset = this.startOffset;
             					    g.DrawPath(pen, graphicsPath);
             					    ```
             					
             					 2. 设置左半部分和右半部分与中间部分样式。
             					
             					    先讲解左边部分:设计思想。就是在管道中间一半的位置,画弧线。看上个绘制矩形拐头的分析图像。那么必然流动线与管道拐头是公用一个圆心。然后矩形的左上角就是在向上圆心的(1/2)的高度,向左(1/2)高度值的地方。旋转半径也是(1/2)高度值。旋转角度是从180°,逆时针旋转90°的地方。千万记住,**图像的绘制是从矩形的左上角的点开始,所以要按照实际情况绘图**。其他的右半部分与中间部分分析方法略
             					
             					    ```c#
             					    graphicsPath.AddArc(new Rectangle(this.Height / 2, this.Height / 2 * (-1) - 1, this.Height, this.Height), 180.0f, -90.0f);
             					    ```
             					
             					   3. 绘制函数  g.DrawPath(pen, graphicsPath);使用画笔参数与绘制路线
             					
             					   4. 使用定时器更改偏移量startOffset,同时需要调用Invalidate方法进行图像的实时更新,所以产生了流动条变动的情况
             					
             					      ```c#
             					             private void Mytimer_Tick(object sender, EventArgs e)
             					              {
             					                  this.startOffset = this.startOffset - this.moveSpeed;
             					      
             					                  if (this.startOffset > this.pipeLength + this.gapLength || this.startOffset < (this.pipeLength + this.gapLength) * (-1))
             					                  {
             					                      this.startOffset = 0.0f;
             					                  }
             					                  this.Invalidate();
             					              }
             					      ```
            
posted @ 2023-03-07 17:59  聆听微风  阅读(231)  评论(0编辑  收藏  举报