在桌面程序编程中,我们经常需要执行耗时较长的代码。为了良好的用户体验,仿照win10加载动画,实现了Loading时异步处理耗时代码。借鉴了网上两个Demo,整理后实现了较好效果,先来看效果图。
先实现了异步开启执行工作任务,然后展示加载动画,待任务执行完毕,关闭动画。
1.画点
using System;
using System.ComponentModel;
using System.Drawing;
namespace Loading
{
///
/// 表示一个"点"
///
internal sealed class LoadingDot
{
#region 字段/属性
[Description("圆心")] private readonly PointF _circleCenter;
[Description("半径")] private readonly float _circleRadius;
///
/// 当前帧绘图坐标,在每次DoAction()时重新计算
///
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 方法
///
/// 重新计算当前帧绘图坐标
///
private void ReCalcLocation()
{
Location = GetDotLocationByAngle(_circleCenter, _circleRadius, _angle);
}
///
/// 点动作
///
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();
}
///
/// 淡出
///
private void FadeOut()
{
if ((_opacity -= AlphaSub) <= 0)
_angle = AppearAngle;
}
///
/// 重置状态
///
public void Reset()
{
_angle = AppearAngle;
_speed = MinSpeed;
_progress = 0;
_opacity = 1;
}
///
/// 加速
///
private void AddSpeed()
{
if (++_speed >= MaxSpeed) _speed = MaxSpeed;
}
///
/// 减速
///
private void SubSpeed()
{
if (--_speed <= MinSpeed) _speed = MinSpeed;
}
#endregion 方法
///
/// 根据半径、角度求圆上坐标
///
/// 圆心
/// 半径
/// 角度
/// 坐标
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
{
///
/// Required designer variable.
///
private System.ComponentModel.IContainer components = null;
///
/// Clean up any resources being used.
///
/// true if managed resources should be disposed; otherwise, false.
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
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
{
///
/// 构造器
///
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;
}
///
/// 构造器
///
///
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 方法
///
/// 设置工作动作
///
///
///
public void SetWorkAction(ParameterizedThreadStart workAction, object arg)
{
_workAction = workAction;
_workActionArg = arg;
}
///
/// 执行工作动作
///
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;
}
}
///
/// 检查是否重置
///
///
private bool CheckToReset()
{
return _dots.Count(d => d.Opacity > 0) == 0;
}
///
/// 初始化点元素
///
private void CreateLoadingDots()
{
for (var i = 0; i < _dots.Length; ++i)
_dots[i] = new LoadingDot(CircleCenter, CircleRadius);
}
///
/// 开始
///
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;
}
///
/// 停止
///
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
{
///
/// 开始加载
///
/// 消息
/// 父窗体
/// 待执行工作
/// 工作参数
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