在設計應用程序過程中,有時候加載對象需時較長,我們可以顯示一個Loading等待頁面,對用戶來說就比較友好了。
這個還是涉及到多線程,下面是步驟。
一、創建好Loading窗體:
一個Panel用於顯示轉圈動畫(仿Win10的Loading),一個Loading文本標簽。動畫的代碼來自網絡。

public partial class Fm20Loading : Form { public Fm20Loading() { InitializeComponent(); //LblMessage.Text = MultiLang.Surface(null, "OnLoading", "目標對象正在加載中, 請您稍等..."); 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.CadetBlue; } /// <summary> /// 構造器 /// </summary> /// <param name="message"></param> public Fm20Loading(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.CadetBlue; Message = message; } private void Fm20Loading_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 Fm20Loading_Shown(object sender, EventArgs e) { if (_workAction != null) { _workThread = new Thread(ExecWorkAction) { 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 重寫 private void LblMessage_DoubleClick(object sender, EventArgs e) { this.Close(); } }

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); } }
二、窗體和動畫有了,怎么使用呢?

private void ShowLoadingForm() { if (Debugger.IsAttached) { return; } Fm20Loading fm20Loading = new Fm20Loading { Name = "Fm20Loading" + DateTime.Now.Ticks }; Thread.Sleep(100); fm20Loading.ShowDialog(); return ; } private void CloseLoadingForm() { if (Debugger.IsAttached) return; for (int i = (Application.OpenForms.Count-1); i >=0; i--) { Form tForm = Application.OpenForms[i]; string fmName = tForm.GetType().Name; if (OString.Left(fmName,11) == "Fm20Loading") { tForm.Close(); } } }
三、調用創建和關閉代碼的代碼(有點繞了)
try { Form child = ActiveChildForm(dllFormNameWithNameSpace); if (child != null) return child; Action handler = new Action(ShowLoadingForm); handler.BeginInvoke(null, null); //在另外一個線程打開,否則會阻塞 Form form = OpenPluginFormInMainDomain(dllFileSimpleName, dllFormNameWithNameSpace, initParam); if (form != null && form is Form) { child = form as Form; ((Fm11Base)child).RightsList = rightsList.ToLower(); ((Fm11Base)child).OnLoadParams = onLoadParams; child.Text = tagTitle; child.MdiParent = (Form)this; child.FormClosed += Child_FormClosed; child.Show(); child.WindowState = FormWindowState.Maximized; this.ActivateMdiChild(child); if (child.HasChildren) { child.Controls[0].Focus(); } CloseLoadingForm(); return child; } else { CloseLoadingForm(); return null; throw new Exception("未找到窗體文件或加載了未知的窗體類型!"); } } catch (Exception ex) { CloseLoadingForm(); MyMsg.Information("窗體實例化出錯,請重試.", ex.Message); return null; }
這部分可以改成你喜歡的使用環境。
如此,一個友好的加載等待頁面就完成了。它和耗時后台任務提示窗口兩種界面結合,可以解決大部分的友好提示界面需求。