(1)背景
在桌面程序开发中,经常需要执行耗时较长的业务代码。为了用户体验友好,需要一个加载动画,实现异步处理耗时代码时显示加载动画。
(2)代码实现
2.1 FrmLoading 前端代码
1 namespace Loading 2 { 3 partial class FrmLoading 4 { 5 /// <summary> 6 /// Required designer variable. 7 /// </summary> 8 private System.ComponentModel.IContainer components = null; 9 10 /// <summary> 11 /// Clean up any resources being used. 12 /// </summary> 13 /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> 14 protected override void Dispose(bool disposing) 15 { 16 if (disposing && (components != null)) 17 { 18 components.Dispose(); 19 } 20 base.Dispose(disposing); 21 } 22 23 #region Windows Form Designer generated code 24 25 /// <summary> 26 /// Required method for Designer support - do not modify 27 /// the contents of this method with the code editor. 28 /// </summary> 29 private void InitializeComponent() 30 { 31 this.LblMessage = new System.Windows.Forms.Label(); 32 this.PnlImage = new System.Windows.Forms.Panel(); 33 this.SuspendLayout(); 34 // 35 // LblMessage 36 // 37 this.LblMessage.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); 38 this.LblMessage.BackColor = System.Drawing.Color.Transparent; 39 this.LblMessage.ForeColor = System.Drawing.Color.White; 40 this.LblMessage.Location = new System.Drawing.Point(36, 224); 41 this.LblMessage.Name = "LblMessage"; 42 this.LblMessage.Size = new System.Drawing.Size(328, 64); 43 this.LblMessage.TabIndex = 0; 44 this.LblMessage.Text = "正在处理中,请稍候……"; 45 this.LblMessage.TextAlign = System.Drawing.ContentAlignment.TopCenter; 46 // 47 // PnlImage 48 // 49 this.PnlImage.Anchor = System.Windows.Forms.AnchorStyles.None; 50 this.PnlImage.BackColor = System.Drawing.Color.Transparent; 51 this.PnlImage.Location = new System.Drawing.Point(100, 12); 52 this.PnlImage.Name = "PnlImage"; 53 this.PnlImage.Size = new System.Drawing.Size(200, 200); 54 this.PnlImage.TabIndex = 1; 55 this.PnlImage.Paint += new System.Windows.Forms.PaintEventHandler(this.PnlImage_Paint); 56 this.PnlImage.Resize += new System.EventHandler(this.PnlImage_Resize); 57 // 58 // FrmLoading 59 // 60 this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 27F); 61 this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 62 this.BackColor = System.Drawing.Color.Black; 63 this.ClientSize = new System.Drawing.Size(400, 300); 64 this.Controls.Add(this.LblMessage); 65 this.Controls.Add(this.PnlImage); 66 this.Font = new System.Drawing.Font("微软雅黑", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); 67 this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; 68 this.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); 69 this.Name = "FrmLoading"; 70 this.Opacity = 0.5D; 71 this.ShowInTaskbar = false; 72 this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; 73 this.Text = "FrmLoading"; 74 this.Load += new System.EventHandler(this.FrmLoading_Load); 75 this.Shown += new System.EventHandler(this.FrmLoading_Shown); 76 this.ResumeLayout(false); 77 78 } 79 80 #endregion 81 82 private System.Windows.Forms.Label LblMessage; 83 private System.Windows.Forms.Panel PnlImage; 84 } 85 }
2.2 FrmLoading后端代码
1 using System; 2 using System.ComponentModel; 3 using System.Drawing; 4 using System.Drawing.Drawing2D; 5 using System.Linq; 6 using System.Threading; 7 using System.Threading.Tasks; 8 using System.Windows.Forms; 9 using ThreadingTimer = System.Threading.Timer; 10 using UITimer = System.Windows.Forms.Timer; 11 12 namespace Loading 13 { 14 public partial class FrmLoading : Form 15 { 16 /// <summary> 17 /// 构造器 18 /// </summary> 19 public FrmLoading() 20 { 21 InitializeComponent(); 22 SetStyle( 23 ControlStyles.AllPaintingInWmPaint | 24 ControlStyles.UserPaint | 25 ControlStyles.OptimizedDoubleBuffer, 26 true); 27 //初始化绘图timer 28 _tmrGraphics = new UITimer { Interval = 1 }; 29 //Invalidate()强制重绘,绘图操作在OnPaint中实现 30 _tmrGraphics.Tick += (sender, e) => PnlImage.Invalidate(false); 31 _dotSize = PnlImage.Width / 10f; 32 //初始化"点" 33 _dots = new LoadingDot[5]; 34 Color = Color.Orange; 35 } 36 37 /// <summary> 38 /// 构造器 39 /// </summary> 40 /// <param name="message"></param> 41 public FrmLoading(string message) 42 { 43 InitializeComponent(); 44 //双缓冲,禁擦背景 45 SetStyle( 46 ControlStyles.AllPaintingInWmPaint | 47 ControlStyles.UserPaint | 48 ControlStyles.OptimizedDoubleBuffer, 49 true); 50 //初始化绘图timer 51 _tmrGraphics = new UITimer { Interval = 1 }; 52 //Invalidate()强制重绘,绘图操作在OnPaint中实现 53 _tmrGraphics.Tick += (sender, e) => PnlImage.Invalidate(false); 54 _dotSize = PnlImage.Width / 10f; 55 //初始化"点" 56 _dots = new LoadingDot[5]; 57 Color = Color.Orange; 58 Message = message; 59 } 60 61 private void FrmLoading_Load(object sender, EventArgs e) 62 { 63 LblMessage.ForeColor = Color; 64 if (Owner != null) 65 { 66 StartPosition = FormStartPosition.Manual; 67 Location = new Point(Owner.Left, Owner.Top); 68 Width = Owner.Width; 69 Height = Owner.Height; 70 } 71 else 72 { 73 var screenRect = Screen.PrimaryScreen.WorkingArea; 74 Location = new Point((screenRect.Width - Width) / 2, (screenRect.Height - Height) / 2); 75 } 76 Start(); 77 } 78 79 private void FrmLoading_Shown(object sender, EventArgs e) 80 { 81 if (_workAction != null) 82 { 83 _workThread = new Thread(ExecWorkAction); 84 _workThread.IsBackground = true; 85 _workThread.Start(); 86 } 87 } 88 89 #region 属性 90 91 [Description("消息")] 92 public string Message 93 { 94 get { return LblMessage.Text; } 95 set { LblMessage.Text = value; } 96 } 97 98 [Browsable(false), Description("圆心")] 99 public PointF CircleCenter => new PointF(PnlImage.Width / 2f, PnlImage.Height / 2f); 100 101 [Browsable(false), Description("半径")] 102 public float CircleRadius => PnlImage.Width / 2f - _dotSize; 103 104 [Browsable(true), Category("Appearance"), Description("设置"点"的前景色")] 105 public Color Color { get; set; } 106 107 #endregion 属性 108 109 #region 字段 110 111 [Description("工作是否完成")] 112 public bool IsWorkCompleted; 113 114 [Description("工作动作")] 115 private ParameterizedThreadStart _workAction; 116 117 [Description("工作动作参数")] 118 private object _workActionArg; 119 120 [Description("工作线程")] 121 private Thread _workThread; 122 123 [Description("工作异常")] 124 public Exception WorkException { get; private set; } 125 126 [Description("点数组")] 127 private readonly LoadingDot[] _dots; 128 129 [Description("UITimer")] 130 private readonly UITimer _tmrGraphics; 131 132 [Description("ThreadingTimer")] 133 private ThreadingTimer _tmrAction; 134 135 [Description("点大小")] 136 private float _dotSize; 137 138 [Description("是否活动")] 139 private bool _isActived; 140 141 [Description("是否绘制:用于状态重置时挂起与恢复绘图")] 142 private bool _isDrawing = true; 143 144 [Description("Timer计数:用于延迟启动每个点 ")] 145 private int _timerCount; 146 147 #endregion 字段 148 149 #region 常量 150 151 [Description("动作间隔(Timer)")] 152 private const int ActionInterval = 30; 153 154 [Description("计数基数:用于计算每个点启动延迟:index * timerCountRadix")] 155 private const int TimerCountRadix = 45; 156 157 #endregion 常量 158 159 #region 方法 160 161 /// <summary> 162 /// 设置工作动作 163 /// </summary> 164 /// <param name="workAction"></param> 165 /// <param name="arg"></param> 166 public void SetWorkAction(ParameterizedThreadStart workAction, object arg) 167 { 168 _workAction = workAction; 169 _workActionArg = arg; 170 } 171 172 /// <summary> 173 /// 执行工作动作 174 /// </summary> 175 private void ExecWorkAction() 176 { 177 try 178 { 179 var workTask = new Task(arg => 180 { 181 _workAction(arg); 182 }, _workActionArg); 183 workTask.Start(); 184 Task.WaitAll(workTask); 185 } 186 catch (Exception exception) 187 { 188 WorkException = exception; 189 } 190 finally 191 { 192 IsWorkCompleted = true; 193 } 194 } 195 196 /// <summary> 197 /// 检查是否重置 198 /// </summary> 199 /// <returns></returns> 200 private bool CheckToReset() 201 { 202 return _dots.Count(d => d.Opacity > 0) == 0; 203 } 204 205 /// <summary> 206 /// 初始化点元素 207 /// </summary> 208 private void CreateLoadingDots() 209 { 210 for (var i = 0; i < _dots.Length; ++i) 211 _dots[i] = new LoadingDot(CircleCenter, CircleRadius); 212 } 213 214 /// <summary> 215 /// 开始 216 /// </summary> 217 public void Start() 218 { 219 CreateLoadingDots(); 220 _timerCount = 0; 221 foreach (var dot in _dots) 222 { 223 dot.Reset(); 224 } 225 _tmrGraphics.Start(); 226 //初始化动作timer 227 _tmrAction = new ThreadingTimer( 228 state => 229 { 230 //动画动作 231 for (var i = 0; i < _dots.Length; i++) 232 { 233 if (_timerCount++ > i * TimerCountRadix) 234 { 235 _dots[i].LoadingDotAction(); 236 } 237 } 238 //是否重置 239 if (CheckToReset()) 240 { 241 //重置前暂停绘图 242 _isDrawing = false; 243 _timerCount = 0; 244 foreach (var dot in _dots) 245 { 246 dot.Reset(); 247 } 248 //恢复绘图 249 _isDrawing = true; 250 } 251 _tmrAction.Change(ActionInterval, Timeout.Infinite); 252 }, 253 null, ActionInterval, Timeout.Infinite); 254 _isActived = true; 255 } 256 257 /// <summary> 258 /// 停止 259 /// </summary> 260 public void Stop() 261 { 262 _tmrGraphics.Stop(); 263 _tmrAction.Dispose(); 264 _isActived = false; 265 } 266 267 #endregion 方法 268 269 #region 重写 270 271 protected override void OnPaint(PaintEventArgs e) 272 { 273 if (IsWorkCompleted) 274 { 275 Stop(); 276 Close(); 277 } 278 } 279 280 private void PnlImage_Paint(object sender, PaintEventArgs e) 281 { 282 if (_isActived && _isDrawing) 283 { 284 //抗锯齿 285 e.Graphics.SmoothingMode = SmoothingMode.HighQuality; 286 using (var bitmap = new Bitmap(200, 200)) 287 { 288 //缓冲绘制 289 using (var bufferGraphics = Graphics.FromImage(bitmap)) 290 { 291 //抗锯齿 292 bufferGraphics.SmoothingMode = SmoothingMode.HighQuality; 293 foreach (var dot in _dots) 294 { 295 var rectangleF = new RectangleF( 296 new PointF(dot.Location.X - _dotSize / 2, dot.Location.Y - _dotSize / 2), 297 new SizeF(_dotSize, _dotSize)); 298 bufferGraphics.FillEllipse(new SolidBrush(Color.FromArgb(dot.Opacity, Color)), 299 rectangleF); 300 } 301 } 302 //贴图 303 e.Graphics.DrawImage(bitmap, new PointF(0, 0)); 304 } //bmp disposed 305 } 306 base.OnPaint(e); 307 } 308 309 private void PnlImage_Resize(object sender, EventArgs e) 310 { 311 PnlImage.Height = PnlImage.Width; 312 _dotSize = PnlImage.Width / 12f; 313 OnResize(e); 314 } 315 316 #endregion 重写 317 } 318 }
2.3 LoadingDot代码
1 using System; 2 using System.ComponentModel; 3 using System.Drawing; 4 5 namespace Loading 6 { 7 /// <summary> 8 /// 表示一个"点" 9 /// </summary> 10 internal sealed class LoadingDot 11 { 12 #region 字段/属性 13 14 [Description("圆心")] 15 private readonly PointF _circleCenter; 16 [Description("半径")] 17 private readonly float _circleRadius; 18 19 /// <summary> 20 /// 当前帧绘图坐标,在每次DoAction()时重新计算 21 /// </summary> 22 public PointF Location; 23 24 [Description("点相对于圆心的角度,用于计算点的绘图坐标")] 25 private int _angle; 26 [Description("透明度")] 27 private int _opacity; 28 [Description("动画进度")] 29 private int _progress; 30 [Description("速度")] 31 private int _speed; 32 33 [Description("透明度")] 34 public int Opacity => _opacity < MinOpacity ? MinOpacity : (_opacity > MaxOpacity ? MaxOpacity : _opacity); 35 36 #endregion 37 38 #region 常量 39 40 [Description("最小速度")] 41 private const int MinSpeed = 2; 42 [Description("最大速度")] 43 private const int MaxSpeed = 11; 44 45 [Description("出现区的相对角度")] 46 private const int AppearAngle = 90; 47 [Description("减速区的相对角度")] 48 private const int SlowAngle = 225; 49 [Description("加速区的相对角度")] 50 private const int QuickAngle = 315; 51 52 [Description("最小角度")] 53 private const int MinAngle = 0; 54 [Description("最大角度")] 55 private const int MaxAngle = 360; 56 57 [Description("淡出速度")] 58 private const int AlphaSub = 25; 59 60 [Description("最小透明度")] 61 private const int MinOpacity = 0; 62 [Description("最大透明度")] 63 private const int MaxOpacity = 255; 64 65 #endregion 常量 66 67 #region 构造器 68 69 public LoadingDot(PointF circleCenter, float circleRadius) 70 { 71 Reset(); 72 _circleCenter = circleCenter; 73 _circleRadius = circleRadius; 74 } 75 76 #endregion 构造器 77 78 #region 方法 79 80 /// <summary> 81 /// 重新计算当前帧绘图坐标 82 /// </summary> 83 private void ReCalcLocation() 84 { 85 Location = GetDotLocationByAngle(_circleCenter, _circleRadius, _angle); 86 } 87 88 /// <summary> 89 /// 点动作 90 /// </summary> 91 public void LoadingDotAction() 92 { 93 switch (_progress) 94 { 95 case 0: 96 { 97 _opacity = MaxOpacity; 98 AddSpeed(); 99 if (_angle + _speed >= SlowAngle && _angle + _speed < QuickAngle) 100 { 101 _progress = 1; 102 _angle = SlowAngle - _speed; 103 } 104 } 105 break; 106 case 1: 107 { 108 SubSpeed(); 109 if (_angle + _speed >= QuickAngle || _angle + _speed < SlowAngle) 110 { 111 _progress = 2; 112 _angle = QuickAngle - _speed; 113 } 114 } 115 break; 116 case 2: 117 { 118 AddSpeed(); 119 if (_angle + _speed >= SlowAngle && _angle + _speed < QuickAngle) 120 { 121 _progress = 3; 122 _angle = SlowAngle - _speed; 123 } 124 } 125 break; 126 case 3: 127 { 128 SubSpeed(); 129 if (_angle + _speed >= QuickAngle && _angle + _speed < MaxAngle) 130 { 131 _progress = 4; 132 _angle = QuickAngle - _speed; 133 } 134 } 135 break; 136 case 4: 137 { 138 SubSpeed(); 139 if (_angle + _speed >= MinAngle && _angle + _speed < AppearAngle) 140 { 141 _progress = 5; 142 _angle = MinAngle; 143 } 144 } 145 break; 146 case 5: 147 { 148 AddSpeed(); 149 FadeOut(); 150 } 151 break; 152 } 153 154 //移动 155 _angle = _angle >= (MaxAngle - _speed) ? MinAngle : _angle + _speed; 156 //重新计算坐标 157 ReCalcLocation(); 158 } 159 160 /// <summary> 161 /// 淡出 162 /// </summary> 163 private void FadeOut() 164 { 165 if ((_opacity -= AlphaSub) <= 0) 166 _angle = AppearAngle; 167 } 168 169 170 /// <summary> 171 /// 重置状态 172 /// </summary> 173 public void Reset() 174 { 175 _angle = AppearAngle; 176 _speed = MinSpeed; 177 _progress = 0; 178 _opacity = 1; 179 } 180 181 /// <summary> 182 /// 加速 183 /// </summary> 184 private void AddSpeed() 185 { 186 if (++_speed >= MaxSpeed) _speed = MaxSpeed; 187 } 188 189 /// <summary> 190 /// 减速 191 /// </summary> 192 private void SubSpeed() 193 { 194 if (--_speed <= MinSpeed) _speed = MinSpeed; 195 } 196 197 #endregion 方法 198 199 /// <summary> 200 /// 根据半径、角度求圆上坐标 201 /// </summary> 202 /// <param name="center">圆心</param> 203 /// <param name="radius">半径</param> 204 /// <param name="angle">角度</param> 205 /// <returns>坐标</returns> 206 public static PointF GetDotLocationByAngle(PointF center, float radius, int angle) 207 { 208 var x = (float)(center.X + radius * Math.Cos(angle * Math.PI / 180)); 209 var y = (float)(center.Y + radius * Math.Sin(angle * Math.PI / 180)); 210 211 return new PointF(x, y); 212 } 213 } 214 }
2.4 LoadingHelper代码
1 using System.Dynamic; 2 using System.Threading; 3 using System.Windows.Forms; 4 5 namespace Loading 6 { 7 public class LoadingHelper 8 { 9 /// <summary> 10 /// 开始加载 11 /// </summary> 12 /// <param name="message">消息</param> 13 /// <param name="ownerForm">父窗体</param> 14 /// <param name="work">待执行工作</param> 15 /// <param name="workArg">工作参数</param> 16 public static void ShowLoading(string message, Form ownerForm, ParameterizedThreadStart work, object workArg = null) 17 { 18 var loadingForm = new FrmLoading(message); 19 dynamic expandoObject = new ExpandoObject(); 20 expandoObject.Form = loadingForm; 21 expandoObject.WorkArg = workArg; 22 loadingForm.SetWorkAction(work, expandoObject); 23 loadingForm.ShowDialog(ownerForm); 24 if (loadingForm.WorkException != null) 25 { 26 throw loadingForm.WorkException; 27 } 28 } 29 } 30 }
(3)调用代码
1 LoadingHelper.ShowLoading("正在加载中,请稍后...", this, o => 2 { 3 //这里写处理耗时的代码,代码处理完成则自动关闭该窗口 4 Thread.Sleep(30000); 5 });
(4)效果