前言
在工业自动化、流程监控等系统中,可视化是提升用户体验和操作效率的重要手段。其中,“流动管道”作为一种常见的界面元素,常用于表示液体、气体的流向或数据流的状态。
本文将详细介绍如何使用 C# 与 GDI+ 技术开发一个功能丰富、性能良好的自定义流动管道控件,涵盖以下核心功能:
-
圆角管道绘制
-
多种流动动画样式(斜线、波浪、点阵)
-
方向指示箭头
-
流动方向控制与动态更新
-
动画流畅性优化
通过本文的学习,可以掌握 GDI+ 图形编程的基础与进阶技巧,并构建出可复用的 UI 控件,应用于实际项目中。
正文
一、基础类定义与控件初始化
我们首先创建一个继承自 Control 的自定义控件类 PipelineControl,并为其添加多个自定义属性以支持灵活配置。
public class PipelineControl : Control
{
private Timer animationTimer;
private float flowOffset = 0;
private const float FLOW_SPEED = 2.0f;
// 自定义属性
private Color pipeColor = Color.DodgerBlue;
private Color flowColor = Color.White;
private bool isHorizontal = true;
private int pipeWidth = 40;
private FlowStyle flowStyle = FlowStyle.Diagonal;
private FlowDirection flowDirection = FlowDirection.RightToLeft;
private readonly int patternRepeat = 3;
private ArrowStyle arrowStyle = ArrowStyle.SolidTriangle;
private int cornerRadius = 0;
public PipelineControl()
{
SetStyle(ControlStyles.SupportsTransparentBackColor |
ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint, true);
BackColor = Color.Transparent;
// 初始化动画计时器
animationTimer = new Timer();
animationTimer.Interval = 50;
animationTimer.Tick += AnimationTimer_Tick;
animationTimer.Start();
}
// 各属性及描述略...
}
所有属性都包含
[Description]特性以便在设计器中显示说明信息,同时修改属性后会调用Invalidate()方法触发重绘。
二、图形绘制实现
1、OnPaint 方法重写
我们重写 OnPaint 方法来完成控件的全部绘制工作,包括:
-
管道主体(支持圆角)
-
渐变填充效果
-
流动动画图案
-
边框绘制
-
方向箭头绘制
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
using (var path = new GraphicsPath())
{
Rectangle pipeRect;
if (IsHorizontal)
{
pipeRect = new Rectangle(0, (Height - PipeWidth) / 2, Width, PipeWidth);
}
else
{
pipeRect = new Rectangle((Width - PipeWidth) / 2, 0, PipeWidth, Height);
}
// 绘制圆角矩形
if (cornerRadius > 0)
{
if (IsHorizontal)
{
path.AddArc(pipeRect.X, pipeRect.Y, cornerRadius * 2, pipeRect.Height, 90, 180);
path.AddArc(pipeRect.Right - cornerRadius * 2, pipeRect.Y, cornerRadius * 2, pipeRect.Height, 270, 180);
path.CloseFigure();
}
else
{
path.AddArc(pipeRect.X, pipeRect.Y, pipeRect.Width, cornerRadius * 2, 180, 180);
path.AddArc(pipeRect.X, pipeRect.Bottom - cornerRadius * 2, pipeRect.Width, cornerRadius * 2, 0, 180);
path.CloseFigure();
}
}
else
{
path.AddRectangle(pipeRect);
}
// 渐变填充
using (var brush = new LinearGradientBrush(pipeRect,
Color.FromArgb(200, PipeColor),
Color.FromArgb(150, PipeColor),
IsHorizontal ? LinearGradientMode.Vertical : LinearGradientMode.Horizontal))
{
e.Graphics.FillPath(brush, path);
}
// 绘制流动动画
DrawFlowAnimation(e.Graphics, pipeRect);
// 绘制边框
using (var pen = new Pen(Color.FromArgb(100, Color.Gray), 1))
{
e.Graphics.DrawPath(pen, path);
}
// 绘制流向箭头
DrawFlowArrow(e.Graphics, pipeRect);
}
}
2、流动动画样式实现
我们实现了三种不同的流动样式:斜线、点阵、波浪线。
每种样式均通过 DrawFlowAnimation 方法统一调度。
private void DrawDiagonalFlow(Graphics g, Rectangle pipeRect)
{
using (var flowBrush = new HatchBrush(HatchStyle.LightDownwardDiagonal,
Color.FromArgb(50, FlowColor), Color.Transparent))
{
g.FillRectangle(flowBrush, pipeRect);
}
}
private void DrawDotsFlow(Graphics g, Rectangle pipeRect)
{
int dotSize = PipeWidth / 4;
using (var dotBrush = new SolidBrush(Color.FromArgb(50, FlowColor)))
{
if (IsHorizontal)
{
for (int x = pipeRect.Left; x < pipeRect.Right; x += PipeWidth)
{
g.FillEllipse(dotBrush, x, pipeRect.Top + (pipeRect.Height - dotSize) / 2, dotSize, dotSize);
}
}
else
{
for (int y = pipeRect.Top; y < pipeRect.Bottom; y += PipeWidth)
{
g.FillEllipse(dotBrush, pipeRect.Left + (pipeRect.Width - dotSize) / 2, y, dotSize, dotSize);
}
}
}
}
private void DrawWaveFlow(Graphics g, Rectangle pipeRect)
{
using (var path = new GraphicsPath())
{
float amplitude = PipeWidth / 4f;
float wavelength = PipeWidth * 2f;
if (IsHorizontal)
{
var points = new List<Point>();
for (float x = pipeRect.Left; x <= pipeRect.Right; x += 5)
{
float y = pipeRect.Top + pipeRect.Height / 2 +
(float)(Math.Sin((x / wavelength) * Math.PI * 2) * amplitude);
points.Add(new Point((int)x, (int)y));
}
path.AddLines(points.ToArray());
}
else
{
var points = new List<Point>();
for (float y = pipeRect.Top; y <= pipeRect.Bottom; y += 5)
{
float x = pipeRect.Left + pipeRect.Width / 2 +
(float)(Math.Sin((y / wavelength) * Math.PI * 2) * amplitude);
points.Add(new Point((int)x, (int)y));
}
path.AddLines(points.ToArray());
}
using (var pen = new Pen(Color.FromArgb(50, FlowColor), 2))
{
g.DrawPath(pen, path);
}
}
}
三、动画控制逻辑
通过一个定时器(Timer)不断改变偏移量 flowOffset 来实现流动动画的连续播放。
private void AnimationTimer_Tick(object sender, EventArgs e)
{
float speed = FLOW_SPEED;
if (flowDirection == FlowDirection.LeftToRight || flowDirection == FlowDirection.TopToBottom)
{
speed = -FLOW_SPEED;
}
flowOffset += speed;
if (IsHorizontal)
{
if (speed > 0 && flowOffset >= Width)
{
flowOffset = -Width / patternRepeat;
}
else if (speed < 0 && flowOffset <= -Width)
{
flowOffset = Width / patternRepeat;
}
}
else
{
if (speed > 0 && flowOffset >= Height)
{
flowOffset = -Height / patternRepeat;
}
else if (speed < 0 && flowOffset <= -Height)
{
flowOffset = Height / patternRepeat;
}
}
Invalidate(); // 触发重绘
}
四、完整代码
namespace AppControls
{
public enum FlowStyle
{
[Description("斜线流动")]
Diagonal,
[Description("点状流动")]
Dots,
[Description("波浪流动")]
Wave,
[Description("虚线流动")]
Dashed
}
public enum FlowDirection
{
[Description("向右")]
RightToLeft,
[Description("向左")]
LeftToRight,
[Description("向上")]
BottomToTop,
[Description("向下")]
TopToBottom
}
public enum ArrowStyle
{
[Description("实心三角形")]
SolidTriangle,
[Description("空心三角形")]
HollowTriangle,
[Description("双线箭头")]
DoubleLines,
[Description("尖头箭头")]
Sharp,
[Description("无箭头")]
None
}
public class PipelineControl : Control
{
private Timer animationTimer;
private float flowOffset = 0;
private const float FLOW_SPEED = 2.0f;
// 自定义属性
private Color pipeColor = Color.DodgerBlue;
private Color flowColor = Color.White;
private bool isHorizontal = true;
private int pipeWidth = 40;
private FlowStyle flowStyle = FlowStyle.Diagonal;
private FlowDirection flowDirection = FlowDirection.RightToLeft;
private readonly int patternRepeat = 3;
private ArrowStyle arrowStyle = ArrowStyle.SolidTriangle;
// 圆角属性
private int cornerRadius = 0;
[Description("管道颜色")]
public Color PipeColor
{
get => pipeColor;
set { pipeColor = value; Invalidate(); }
}
[Description("流动效果颜色")]
public Color FlowColor
{
get => flowColor;
set { flowColor = value; Invalidate(); }
}
[Description("是否横向显示")]
public bool IsHorizontal
{
get => isHorizontal;
set
{
isHorizontal = value;
// 根据方向自动调整流向
if (isHorizontal)
{
flowDirection = FlowDirection.RightToLeft;
}
else
{
flowDirection = FlowDirection.TopToBottom;
}
Invalidate();
}
}
[Description("管道宽度")]
public int PipeWidth
{
get => pipeWidth;
set { pipeWidth = value; Invalidate(); }
}
[Description("流动样式")]
public FlowStyle FlowStyle
{
get => flowStyle;
set { flowStyle = value; Invalidate(); }
}
[Description("流动方向")]
public FlowDirection FlowDirection
{
get => flowDirection;
set
{
flowDirection = value;
// 根据流向自动调整方向
isHorizontal = (value == FlowDirection.LeftToRight || value == FlowDirection.RightToLeft);
Invalidate();
}
}
[Description("箭头样式")]
public ArrowStyle ArrowStyle
{
get => arrowStyle;
set { arrowStyle = value; Invalidate(); }
}
[Description("管道的圆角半径")]
public int CornerRadius
{
get => cornerRadius;
set
{
cornerRadius = Math.Max(0, Math.Min(value, PipeWidth)); // 限制圆角半径不超过管道宽度
Invalidate();
}
}
public PipelineControl()
{
SetStyle(ControlStyles.SupportsTransparentBackColor |
ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint, true);
BackColor = Color.Transparent;
// 初始化动画计时器
animationTimer = new Timer();
animationTimer.Interval = 50;
animationTimer.Tick += AnimationTimer_Tick;
animationTimer.Start();
}
private void AnimationTimer_Tick(object sender, EventArgs e)
{
float speed = FLOW_SPEED;
if (flowDirection == FlowDirection.LeftToRight || flowDirection == FlowDirection.TopToBottom)
{
speed = -FLOW_SPEED;
}
flowOffset += speed;
if (IsHorizontal)
{
if (speed > 0 && flowOffset >= Width)
{
flowOffset = -Width / patternRepeat;
}
else if (speed < 0 && flowOffset <= -Width)
{
flowOffset = Width / patternRepeat;
}
}
else
{
if (speed > 0 && flowOffset >= Height)
{
flowOffset = -Height / patternRepeat;
}
else if (speed < 0 && flowOffset <= -Height)
{
flowOffset = Height / patternRepeat;
}
}
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
using (var path = new GraphicsPath())
{
Rectangle pipeRect;
if (IsHorizontal)
{
pipeRect = new Rectangle(0, (Height - PipeWidth) / 2, Width, PipeWidth);
}
else
{
pipeRect = new Rectangle((Width - PipeWidth) / 2, 0, PipeWidth, Height);
}
if (cornerRadius > 0)
{
if (IsHorizontal)
{
// 水平管道的圆角矩形
path.AddArc(pipeRect.X, pipeRect.Y, cornerRadius * 2, pipeRect.Height, 90, 180); // 左端
path.AddArc(pipeRect.Right - cornerRadius * 2, pipeRect.Y, cornerRadius * 2, pipeRect.Height, 270, 180); // 右端
path.CloseFigure();
}
else
{
// 垂直管道的圆角矩形
path.AddArc(pipeRect.X, pipeRect.Y, pipeRect.Width, cornerRadius * 2, 180, 180); // 上端
path.AddArc(pipeRect.X, pipeRect.Bottom - cornerRadius * 2, pipeRect.Width, cornerRadius * 2, 0, 180); // 下端
path.CloseFigure();
}
}
else
{
// 绘制管道主体
path.AddRectangle(pipeRect);
}
// 绘制管道主体渐变填充
using (var brush = new LinearGradientBrush(pipeRect,
Color.FromArgb(200, PipeColor),
Color.FromArgb(150, PipeColor),
IsHorizontal ? LinearGradientMode.Vertical : LinearGradientMode.Horizontal))
{
e.Graphics.FillPath(brush, path);
}
// 绘制流动效果
DrawFlowAnimation(e.Graphics, pipeRect);
// 绘制边框
using (var pen = new Pen(Color.FromArgb(100, Color.Gray), 1))
{
e.Graphics.DrawPath(pen, path);
}
// 绘制流向箭头
DrawFlowArrow(e.Graphics, pipeRect);
}
}
private void DrawFlowAnimation(Graphics g, Rectangle pipeRect)
{
// 创建扩展的绘制区域,以实现无缝连接
Rectangle extendedRect;
if (IsHorizontal)
{
extendedRect = new Rectangle(
-pipeRect.Width,
pipeRect.Y,
pipeRect.Width * (patternRepeat + 1),
pipeRect.Height);
}
else
{
extendedRect = new Rectangle(
pipeRect.X,
-pipeRect.Height,
pipeRect.Width,
pipeRect.Height * (patternRepeat + 1));
}
// 使用裁剪区域确保只显示控件范围内的内容
g.SetClip(pipeRect);
Matrix matrix = new Matrix();
if (IsHorizontal)
{
matrix.Translate(flowOffset, 0);
}
else
{
matrix.Translate(0, flowOffset);
}
g.Transform = matrix;
switch (FlowStyle)
{
case FlowStyle.Diagonal:
DrawDiagonalFlow(g, extendedRect);
break;
case FlowStyle.Dots:
DrawDotsFlow(g, extendedRect);
break;
case FlowStyle.Wave:
DrawWaveFlow(g, extendedRect);
break;
case FlowStyle.Dashed:
DrawDashedFlow(g, extendedRect);
break;
}
g.ResetTransform();
g.ResetClip();
}
private void DrawDiagonalFlow(Graphics g, Rectangle pipeRect)
{
using (var flowBrush = new HatchBrush(HatchStyle.LightDownwardDiagonal,
Color.FromArgb(50, FlowColor), Color.Transparent))
{
g.FillRectangle(flowBrush, pipeRect);
}
}
private void DrawDotsFlow(Graphics g, Rectangle pipeRect)
{
int dotSize = PipeWidth / 4;
using (var dotBrush = new SolidBrush(Color.FromArgb(50, FlowColor)))
{
if (IsHorizontal)
{
for (int x = pipeRect.Left; x < pipeRect.Right; x += PipeWidth)
{
g.FillEllipse(dotBrush, x, pipeRect.Top + (pipeRect.Height - dotSize) / 2, dotSize, dotSize);
}
}
else
{
for (int y = pipeRect.Top; y < pipeRect.Bottom; y += PipeWidth)
{
g.FillEllipse(dotBrush, pipeRect.Left + (pipeRect.Width - dotSize) / 2, y, dotSize, dotSize);
}
}
}
}
private void DrawWaveFlow(Graphics g, Rectangle pipeRect)
{
using (var path = new GraphicsPath())
{
float amplitude = PipeWidth / 4f;
float wavelength = PipeWidth * 2f;
if (IsHorizontal)
{
var points = new List<Point>();
for (float x = pipeRect.Left; x <= pipeRect.Right; x += 5)
{
float y = pipeRect.Top + pipeRect.Height / 2 +
(float)(Math.Sin((x / wavelength) * Math.PI * 2) * amplitude);
points.Add(new Point((int)x, (int)y));
}
path.AddLines(points.ToArray());
}
else
{
var points = new List<Point>();
for (float y = pipeRect.Top; y <= pipeRect.Bottom; y += 5)
{
float x = pipeRect.Left + pipeRect.Width / 2 +
(float)(Math.Sin((y / wavelength) * Math.PI * 2) * amplitude);
points.Add(new Point((int)x, (int)y));
}
path.AddLines(points.ToArray());
}
using (var pen = new Pen(Color.FromArgb(50, FlowColor), 2))
{
g.DrawPath(pen, path);
}
}
}
private void DrawDashedFlow(Graphics g, Rectangle pipeRect)
{
using (var pen = new Pen(Color.FromArgb(50, FlowColor), 2))
{
pen.DashStyle = DashStyle.Dash;
pen.DashPattern = new float[] { 10.0f, 10.0f }; // 设置虚线样式
if (IsHorizontal)
{
float centerY = pipeRect.Top + pipeRect.Height / 2;
for (int x = pipeRect.Left; x < pipeRect.Right; x += 40)
{
g.DrawLine(pen, x, centerY, x + 20, centerY);
}
}
else
{
float centerX = pipeRect.Left + pipeRect.Width / 2;
for (int y = pipeRect.Top; y < pipeRect.Bottom; y += 40)
{
g.DrawLine(pen, centerX, y, centerX, y + 20);
}
}
}
}
private void DrawFlowArrow(Graphics g, Rectangle pipeRect)
{
if (arrowStyle == ArrowStyle.None) return;
DrawSingleArrow(g, pipeRect);
}
private void DrawSingleArrow(Graphics g, Rectangle pipeRect)
{
if (arrowStyle == ArrowStyle.None) return;
int arrowSize = PipeWidth / 2;
Point[] arrowPoints = new Point[3];
Point arrowCenter;
// 根据方向计算箭头中心点和基本方向
switch (FlowDirection)
{
case FlowDirection.RightToLeft:
arrowCenter = new Point(pipeRect.Right - arrowSize * 2, pipeRect.Top + pipeRect.Height / 2);
CalculateArrowPoints(arrowCenter, arrowSize, 0, out arrowPoints);
break;
case FlowDirection.LeftToRight:
arrowCenter = new Point(pipeRect.Left + arrowSize * 2, pipeRect.Top + pipeRect.Height / 2);
CalculateArrowPoints(arrowCenter, arrowSize, 180, out arrowPoints);
break;
case FlowDirection.TopToBottom:
arrowCenter = new Point(pipeRect.Left + pipeRect.Width / 2, pipeRect.Top + arrowSize * 2);
CalculateArrowPoints(arrowCenter, arrowSize, 270, out arrowPoints);
break;
case FlowDirection.BottomToTop:
arrowCenter = new Point(pipeRect.Left + pipeRect.Width / 2, pipeRect.Bottom - arrowSize * 2);
CalculateArrowPoints(arrowCenter, arrowSize, 90, out arrowPoints);
break;
default:
return;
}
// 根据不同样式绘制箭头
switch (ArrowStyle)
{
case ArrowStyle.SolidTriangle:
DrawSolidTriangle(g, arrowPoints);
break;
case ArrowStyle.HollowTriangle:
DrawHollowTriangle(g, arrowPoints);
break;
case ArrowStyle.DoubleLines:
DrawDoubleLines(g, arrowPoints);
break;
case ArrowStyle.Sharp:
DrawSharpArrow(g, arrowPoints);
break;
}
}
private void DrawArrowWithStyle(Graphics g, Point[] points)
{
switch (ArrowStyle)
{
case ArrowStyle.SolidTriangle:
DrawSolidTriangle(g, points);
break;
case ArrowStyle.HollowTriangle:
DrawHollowTriangle(g, points);
break;
case ArrowStyle.DoubleLines:
DrawDoubleLines(g, points);
break;
case ArrowStyle.Sharp:
DrawSharpArrow(g, points);
break;
}
}
private void CalculateArrowPoints(Point center, int size, float angleDegrees, out Point[] points)
{
points = new Point[3];
float angleRad = angleDegrees * (float)Math.PI / 180f;
// 计算基本三角形的三个点
points[0] = new Point( // 箭头尖端
(int)(center.X + size * Math.Cos(angleRad)),
(int)(center.Y + size * Math.Sin(angleRad))
);
points[1] = new Point( // 左翼
(int)(center.X + size * Math.Cos(angleRad + 2.618f)),
(int)(center.Y + size * Math.Sin(angleRad + 2.618f))
);
points[2] = new Point( // 右翼
(int)(center.X + size * Math.Cos(angleRad - 2.618f)),
(int)(center.Y + size * Math.Sin(angleRad - 2.618f))
);
}
private void DrawSolidTriangle(Graphics g, Point[] points)
{
using (var brush = new SolidBrush(Color.FromArgb(180, PipeColor)))
{
g.FillPolygon(brush, points);
}
}
private void DrawHollowTriangle(Graphics g, Point[] points)
{
using (var pen = new Pen(Color.FromArgb(180, PipeColor), 2))
{
g.DrawPolygon(pen, points);
}
}
private void DrawDoubleLines(Graphics g, Point[] points)
{
using (var pen = new Pen(Color.FromArgb(180, PipeColor), 2))
{
// 计算两条平行线的点
Point center = points[0];
Point left = points[1];
Point right = points[2];
// 绘制外侧线
g.DrawLine(pen, left, center);
g.DrawLine(pen, right, center);
// 计算并绘制内侧线
Point innerLeft = new Point(
(left.X + center.X) / 2,
(left.Y + center.Y) / 2
);
Point innerRight = new Point(
(right.X + center.X) / 2,
(right.Y + center.Y) / 2
);
g.DrawLine(pen, innerLeft, center);
g.DrawLine(pen, innerRight, center);
}
}
private void DrawSharpArrow(Graphics g, Point[] points)
{
using (var pen = new Pen(Color.FromArgb(180, PipeColor), 2))
{
// 计算箭头主干的终点
Point tailEnd = new Point(
(points[1].X + points[2].X) / 2,
(points[1].Y + points[2].Y) / 2
);
// 绘制箭头主干
g.DrawLine(pen, tailEnd, points[0]);
// 绘制箭头两翼
g.DrawLine(pen, points[0], points[1]);
g.DrawLine(pen, points[0], points[2]);
}
}
private Point[] CalculateArrowHead(Point tip, float length, FlowDirection direction)
{
Point[] points = new Point[2];
double angle = Math.PI / 6; // 30度角
switch (direction)
{
case FlowDirection.RightToLeft:
points[0] = new Point(
(int)(tip.X - length * Math.Cos(angle)),
(int)(tip.Y - length * Math.Sin(angle))
);
points[1] = new Point(
(int)(tip.X - length * Math.Cos(angle)),
(int)(tip.Y + length * Math.Sin(angle))
);
break;
case FlowDirection.LeftToRight:
points[0] = new Point(
(int)(tip.X + length * Math.Cos(angle)),
(int)(tip.Y - length * Math.Sin(angle))
);
points[1] = new Point(
(int)(tip.X + length * Math.Cos(angle)),
(int)(tip.Y + length * Math.Sin(angle))
);
break;
case FlowDirection.TopToBottom:
points[0] = new Point(
(int)(tip.X - length * Math.Sin(angle)),
(int)(tip.Y - length * Math.Cos(angle))
);
points[1] = new Point(
(int)(tip.X + length * Math.Sin(angle)),
(int)(tip.Y - length * Math.Cos(angle))
);
break;
case FlowDirection.BottomToTop:
points[0] = new Point(
(int)(tip.X - length * Math.Sin(angle)),
(int)(tip.Y + length * Math.Cos(angle))
);
points[1] = new Point(
(int)(tip.X + length * Math.Sin(angle)),
(int)(tip.Y + length * Math.Cos(angle))
);
break;
}
return points;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
animationTimer?.Stop();
animationTimer?.Dispose();
}
base.Dispose(disposing);
}
}
}
五、效果展示
总结
本文通过 C# 与 GDI+ 技术,详细讲解了一个可用于工业自动化领域的自定义流动管道控件的实现过程。
我们从基础类设计入手,逐步完成了以下关键模块:
-
可配置的控件属性管理
-
支持圆角的管道绘制
-
多样化的流动动画样式
-
动态方向控制与动画循环机制
-
箭头提示功能
该控件具备良好的扩展性,可以根据需求进一步添加如"多段管道连接"、"状态颜色变化"等功能。希望本篇文章能帮助大家深入理解 GDI+ 编程技巧,并在实际项目中加以应用。
最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。
也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!
作者:技术老小子
出处:mp.weixin.qq.com/s/_AVBlOavArUSiEAkDZ3HkA
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!