在桌面程序编程中,我们经常需要执行耗时较长的代码。为了良好的用户体验,仿照win10加载动画,实现了Loading时异步处理耗时代码。借鉴了网上两个Demo,整理后实现了较好效果,先来看效果图。
先实现了异步开启执行工作任务,然后展示加载动画,待任务执行完毕,关闭动画。
1.画点
using System; using System.ComponentModel; using System.Drawing; namespace Loading { /// <summary> /// 表示一个"点" /// </summary> internal sealed class LoadingDot { #region 字段/属性 [Description("圆心")] private readonly PointF _circleCenter; [Description("半径")] private readonly float _circleRadius; /// <summary> /// 当前帧绘图坐标,在每次DoAction()时重新计算 /// </summary> public PointF Location; [Description("点相对于圆心的角度,用于计算点的绘图坐标")] private int _angle; [Description("透明度")] private int _opacity; [Description("动画进度")] private int _progress; [Description("速度")] private int _speed; [Description("透明度")] public int Opacity => _opacity < MinOpacity ? MinOpacity : (_opacity > MaxOpacity ? MaxOpacity : _opacity); #endregion #region 常量 [Description("最小速度")] private const int MinSpeed = 2; [Description("最大速度")] private const int MaxSpeed = 11; [Description("出现区的相对角度")] private const int AppearAngle = 90; [Description("减速区的相对角度")] private const int SlowAngle = 225; [Description("加速区的相对角度")] private const int QuickAngle = 315; [Description("最小角度")] private const int MinAngle = 0; [Description("最大角度")] private const int MaxAngle = 360; [Description("淡出速度")] private const int AlphaSub = 25; [Description("最小透明度")] private const int MinOpacity = 0; [Description("最大透明度")] private const int MaxOpacity = 255; #endregion 常量 #region 构造器 public LoadingDot(PointF circleCenter, float circleRadius) { Reset(); _circleCenter = circleCenter; _circleRadius = circleRadius; } #endregion 构造器 #region 方法 /// <summary> /// 重新计算当前帧绘图坐标 /// </summary> private void ReCalcLocation() { Location = GetDotLocationByAngle(_circleCenter, _circleRadius, _angle); } /// <summary> /// 点动作 /// </summary> public void LoadingDotAction() { switch (_progress) { case 0: { _opacity = MaxOpacity; AddSpeed(); if (_angle + _speed >= SlowAngle && _angle + _speed < QuickAngle) { _progress = 1; _angle = SlowAngle - _speed; } } break; case 1: { SubSpeed(); if (_angle + _speed >= QuickAngle || _angle + _speed < SlowAngle) { _progress = 2; _angle = QuickAngle - _speed; } } break; case 2: { AddSpeed(); if (_angle + _speed >= SlowAngle && _angle + _speed < QuickAngle) { _progress = 3; _angle = SlowAngle - _speed; } } break; case 3: { SubSpeed(); if (_angle + _speed >= QuickAngle && _angle + _speed < MaxAngle) { _progress = 4; _angle = QuickAngle - _speed; } } break; case 4: { SubSpeed(); if (_angle + _speed >= MinAngle && _angle + _speed < AppearAngle) { _progress = 5; _angle = MinAngle; } } break; case 5: { AddSpeed(); FadeOut(); } break; } //移动 _angle = _angle >= (MaxAngle - _speed) ? MinAngle : _angle + _speed; //重新计算坐标 ReCalcLocation(); } /// <summary> /// 淡出 /// </summary> private void FadeOut() { if ((_opacity -= AlphaSub) <= 0) _angle = AppearAngle; } /// <summary> /// 重置状态 /// </summary> public void Reset() { _angle = AppearAngle; _speed = MinSpeed; _progress = 0; _opacity = 1; } /// <summary> /// 加速 /// </summary> private void AddSpeed() { if (++_speed >= MaxSpeed) _speed = MaxSpeed; } /// <summary> /// 减速 /// </summary> private void SubSpeed() { if (--_speed <= MinSpeed) _speed = MinSpeed; } #endregion 方法 /// <summary> /// 根据半径、角度求圆上坐标 /// </summary> /// <param name="center">圆心</param> /// <param name="radius">半径</param> /// <param name="angle">角度</param> /// <returns>坐标</returns> public static PointF GetDotLocationByAngle(PointF center, float radius, int angle) { var x = (float) (center.X + radius*Math.Cos(angle*Math.PI/180)); var y = (float) (center.Y + radius*Math.Sin(angle*Math.PI/180)); return new PointF(x, y); } } }2.创建Loading窗体,包括遮罩层,加载动画,文字提示。
namespace Loading { partial class FrmLoading { /// <summary> /// Required designer variable. /// </summary> private System.ComponentModel.IContainer components = null; /// <summary> /// Clean up any resources being used. /// </summary> /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows Form Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { this.LblMessage = new System.Windows.Forms.Label(); this.PnlImage = new System.Windows.Forms.Panel(); this.SuspendLayout(); // // LblMessage // this.LblMessage.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); this.LblMessage.BackColor = System.Drawing.Color.Transparent; this.LblMessage.ForeColor = System.Drawing.Color.White; this.LblMessage.Location = new System.Drawing.Point(36, 224); this.LblMessage.Name = "LblMessage"; this.LblMessage.Size = new System.Drawing.Size(328, 64); this.LblMessage.TabIndex = 0; this.LblMessage.Text = "正在处理中,请稍候……"; this.LblMessage.TextAlign = System.Drawing.ContentAlignment.TopCenter; // // PnlImage // this.PnlImage.Anchor = System.Windows.Forms.AnchorStyles.None; this.PnlImage.BackColor = System.Drawing.Color.Transparent; this.PnlImage.Location = new System.Drawing.Point(100, 12); this.PnlImage.Name = "PnlImage"; this.PnlImage.Size = new System.Drawing.Size(200, 200); this.PnlImage.TabIndex = 1; this.PnlImage.Paint += new System.Windows.Forms.PaintEventHandler(this.PnlImage_Paint); this.PnlImage.Resize += new System.EventHandler(this.PnlImage_Resize); // // FrmLoading // this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 27F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.BackColor = System.Drawing.Color.Black; this.ClientSize = new System.Drawing.Size(400, 300); this.Controls.Add(this.LblMessage); this.Controls.Add(this.PnlImage); this.Font = new System.Drawing.Font("微软雅黑", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; this.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.Name = "FrmLoading"; this.Opacity = 0.5D; this.ShowInTaskbar = false; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.Text = "FrmLoading"; this.Load += new System.EventHandler(this.FrmLoading_Load); this.Shown += new System.EventHandler(this.FrmLoading_Shown); this.ResumeLayout(false); } #endregion private System.Windows.Forms.Label LblMessage; private System.Windows.Forms.Panel PnlImage; } }
using System; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using ThreadingTimer = System.Threading.Timer; using UITimer = System.Windows.Forms.Timer; namespace Loading { public partial class FrmLoading : Form { /// <summary> /// 构造器 /// </summary> public FrmLoading() { InitializeComponent(); SetStyle( ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true); //初始化绘图timer _tmrGraphics = new UITimer { Interval = 1 }; //Invalidate()强制重绘,绘图操作在OnPaint中实现 _tmrGraphics.Tick += (sender, e) => PnlImage.Invalidate(false); _dotSize = PnlImage.Width / 10f; //初始化"点" _dots = new LoadingDot[5]; Color = Color.Orange; } /// <summary> /// 构造器 /// </summary> /// <param name="message"></param> public FrmLoading(string message) { InitializeComponent(); //双缓冲,禁擦背景 SetStyle( ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true); //初始化绘图timer _tmrGraphics = new UITimer {Interval = 1}; //Invalidate()强制重绘,绘图操作在OnPaint中实现 _tmrGraphics.Tick += (sender, e) => PnlImage.Invalidate(false); _dotSize = PnlImage.Width/10f; //初始化"点" _dots = new LoadingDot[5]; Color = Color.Orange; Message = message; } private void FrmLoading_Load(object sender, EventArgs e) { LblMessage.ForeColor = Color; if (Owner != null) { StartPosition = FormStartPosition.Manual; Location = new Point(Owner.Left, Owner.Top); Width = Owner.Width; Height = Owner.Height; } else { var screenRect = Screen.PrimaryScreen.WorkingArea; Location = new Point((screenRect.Width - Width) / 2, (screenRect.Height - Height) / 2); } Start(); } private void FrmLoading_Shown(object sender, EventArgs e) { if (_workAction != null) { _workThread = new Thread(ExecWorkAction); _workThread.IsBackground = true; _workThread.Start(); } } #region 属性 [Description("消息")] public string Message { get { return LblMessage.Text; } set { LblMessage.Text = value; } } [Browsable(false), Description("圆心")] public PointF CircleCenter => new PointF(PnlImage.Width /2f, PnlImage.Height /2f); [Browsable(false), Description("半径")] public float CircleRadius => PnlImage.Width /2f - _dotSize; [Browsable(true), Category("Appearance"), Description("设置\"点\"的前景色")] public Color Color { get; set; } #endregion 属性 #region 字段 [Description("工作是否完成")] public bool IsWorkCompleted; [Description("工作动作")] private ParameterizedThreadStart _workAction; [Description("工作动作参数")] private object _workActionArg; [Description("工作线程")] private Thread _workThread; [Description("工作异常")] public Exception WorkException { get; private set; } [Description("点数组")] private readonly LoadingDot[] _dots; [Description("UITimer")] private readonly UITimer _tmrGraphics; [Description("ThreadingTimer")] private ThreadingTimer _tmrAction; [Description("点大小")] private float _dotSize; [Description("是否活动")] private bool _isActived; [Description("是否绘制:用于状态重置时挂起与恢复绘图")] private bool _isDrawing = true; [Description("Timer计数:用于延迟启动每个点 ")] private int _timerCount; #endregion 字段 #region 常量 [Description("动作间隔(Timer)")] private const int ActionInterval = 30; [Description("计数基数:用于计算每个点启动延迟:index * timerCountRadix")] private const int TimerCountRadix = 45; #endregion 常量 #region 方法 /// <summary> /// 设置工作动作 /// </summary> /// <param name="workAction"></param> /// <param name="arg"></param> public void SetWorkAction(ParameterizedThreadStart workAction, object arg) { _workAction = workAction; _workActionArg = arg; } /// <summary> /// 执行工作动作 /// </summary> private void ExecWorkAction() { try { var workTask = new Task(arg => { _workAction(arg); }, _workActionArg); workTask.Start(); Task.WaitAll(workTask); } catch (Exception exception) { WorkException = exception; } finally { IsWorkCompleted = true; } } /// <summary> /// 检查是否重置 /// </summary> /// <returns></returns> private bool CheckToReset() { return _dots.Count(d => d.Opacity > 0) == 0; } /// <summary> /// 初始化点元素 /// </summary> private void CreateLoadingDots() { for (var i = 0; i < _dots.Length; ++i) _dots[i] = new LoadingDot(CircleCenter, CircleRadius); } /// <summary> /// 开始 /// </summary> public void Start() { CreateLoadingDots(); _timerCount = 0; foreach (var dot in _dots) { dot.Reset(); } _tmrGraphics.Start(); //初始化动作timer _tmrAction = new ThreadingTimer( state => { //动画动作 for (var i = 0; i < _dots.Length; i++) { if (_timerCount++ > i*TimerCountRadix) { _dots[i].LoadingDotAction(); } } //是否重置 if (CheckToReset()) { //重置前暂停绘图 _isDrawing = false; _timerCount = 0; foreach (var dot in _dots) { dot.Reset(); } //恢复绘图 _isDrawing = true; } _tmrAction.Change(ActionInterval, Timeout.Infinite); }, null, ActionInterval, Timeout.Infinite); _isActived = true; } /// <summary> /// 停止 /// </summary> public void Stop() { _tmrGraphics.Stop(); _tmrAction.Dispose(); _isActived = false; } #endregion 方法 #region 重写 protected override void OnPaint(PaintEventArgs e) { if (IsWorkCompleted) { Stop(); Close(); } } private void PnlImage_Paint(object sender, PaintEventArgs e) { if (_isActived && _isDrawing) { //抗锯齿 e.Graphics.SmoothingMode = SmoothingMode.HighQuality; using (var bitmap = new Bitmap(200, 200)) { //缓冲绘制 using (var bufferGraphics = Graphics.FromImage(bitmap)) { //抗锯齿 bufferGraphics.SmoothingMode = SmoothingMode.HighQuality; foreach (var dot in _dots) { var rectangleF = new RectangleF( new PointF(dot.Location.X - _dotSize / 2, dot.Location.Y - _dotSize / 2), new SizeF(_dotSize, _dotSize)); bufferGraphics.FillEllipse(new SolidBrush(Color.FromArgb(dot.Opacity, Color)), rectangleF); } } //贴图 e.Graphics.DrawImage(bitmap, new PointF(0, 0)); } //bmp disposed } base.OnPaint(e); } private void PnlImage_Resize(object sender, EventArgs e) { PnlImage.Height = PnlImage.Width; _dotSize = PnlImage.Width / 12f; OnResize(e); } #endregion 重写 } }3.执行工作,展示Loading
using System.Dynamic; using System.Threading; using System.Windows.Forms; namespace Loading { public class LoadingHelper { /// <summary> /// 开始加载 /// </summary> /// <param name="message">消息</param> /// <param name="ownerForm">父窗体</param> /// <param name="work">待执行工作</param> /// <param name="workArg">工作参数</param> public static void ShowLoading(string message, Form ownerForm, ParameterizedThreadStart work, object workArg = null) { var loadingForm = new FrmLoading(message); dynamic expandoObject = new ExpandoObject(); expandoObject.Form = loadingForm; expandoObject.WorkArg = workArg; loadingForm.SetWorkAction(work, expandoObject); loadingForm.ShowDialog(ownerForm); if (loadingForm.WorkException != null) { throw loadingForm.WorkException; } } } }4.调用
LoadingHelper.ShowLoading("有朋自远方来,不亦乐乎。", this, (obj) => { //这里写处理耗时的代码,代码处理完成则自动关闭该窗口 Thread.Sleep(10000); });github地址: https://github.com/afresh/Loading