1. 程式人生 > >C# Winform 載入窗體/物件時的等待頁面設計

C# Winform 載入窗體/物件時的等待頁面設計

在設計應用程式過程中,有時候載入物件需時較長,我們可以顯示一個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(); } }
Loading窗體程式碼
    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);
        }
    }
繪製圓點動畫的LoadingDot程式碼

二、窗體和動畫有了,怎麼使用呢?

        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;
                }

  這部分可以改成你喜歡的使用環境。

  如此,一個友好的載入等待頁面就完成了。它和耗時後臺任務提示視窗兩種介面結合,可以解決大部分的友好提示介面需求。